diff --git a/README.md b/README.md index fc36472..640eb59 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ -To obtain new Auth Code : +To obtain client ID and secret : +https://developer.amazon.com/lwa/sp/overview.html +Click create new security profile, and add whitelist redirect uri : https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO -https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO + +To obtain new Auth Code : + +https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO Response URL (Decoded) : diff --git a/backend/config/config.js b/backend/config/config.js index 67573de..b0dcc64 100644 --- a/backend/config/config.js +++ b/backend/config/config.js @@ -3,15 +3,28 @@ var config = {}; config.DB_URL = 'mongodb://localhost:27017/tellall'; config.PORT = 5000; -config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt'; -config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH'; +//Bilal TOKENS +//config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt'; +//config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH'; + +//Saburly TOKENS +config.TOKEN = 'Atza|IwEBIABS0RvlVshGGO64X0tabhUuzpJKjbWNgxpRiy7YTftFD_lWlp-cbeXuVjRAu8kior2W2C5swf_rEHYvELQXdw78gB9WJQh4ITncPgqNCxvEnwVpIXiyeC_O287DRErnTYbI3s34i4NcxrXzobB8fIpTZxOkg6BI6vQGvvaiNLsTim2ElDYgAEmdgTN77llcMai521aovDqEw_XFc6GspeXhiGKxRomCMIL2UaT649owapDQ3y3Ug9eHvEaBzqjYdOUAtRtv19BGkG8YPs3npHmP5AD0Oc3ByCfrofcGk7fdd_nq28pRX6h4LXk4ylM279qlneWh9EErsWh8vtWuGEGusnDxW17OzEzf7HuwNDqdCJ6gCrIEkZaHISrSQ-vTsYGhKbv0z4nNjf_W_aoc9UJr9LnISCXx424R--iGDKZXhYWlZRjaiHsXE33MpS_M-sdN6GXYQwIjanTUahVXh5h-IBam5uJzTejE6CkIh5iUJ6um2IlDelJRMGS-T_aaG3zUvEagvEd9V9Z5mVN_kmO8bH4H2VefZuFGHRsCPa6SoLrlN0rkXK5fMw-zXfV2MHvQFdkgqYqGKxiEwWJ-g4n1ZrcPtWQowHT2z2yWrfnM2A6g8GIdPT23znmRcrdz4EU'; +config.REFRESH_TOKEN = 'Atzr|IwEBIENdBZntrzvJYesv8SGhnty4Nyk2ZySL09elw5N0wH8S1Brz1UgIYLqenw3sKKxnc-VrIUbNtl1Ka4GDKwcTr2fDU_AbKQ6YXzeRBrfRQVvNOeCtjZE8P6Kg1PxAeQoCsqo7WPxK8ZdUaLwPjt_xiZ1FXtr01g-211PJs4KEg5jyF5nY2S14jA_TbwDW6ihpNqWd6ZklTZSRaOeSGa1mXZCSZ5yTsZIQV1Pn0fKhCXtcVg2L833YqRmextmHij4-2NtBQdW3gif5MPdhYjTqDNwgxO3OOagK1uSFqXOnMcmEDnxZuQQApugfDzClfN6DiDALCKN4dVAX8-OU_L2xsUkKiFP9rQjvHWJoRFBT1FpXjBfoVyzM1AaJ6C83WX6SjOBE0hhikQKIaPSe1ikK8_MzOIs2wqLLPnLGnKj3kcKMkDmY6DMgxfWj2H0hwLy0oZ7_qykS7wUHMGjRO5yScuBWFIr0RFFu00GS7zrKjkhFc_E4ZBBKskn0gywS-5pogo0U1rQtLg8lQJsVbxXQwtq2TxkGFMBiVxtQcXtHM1qbCVXpZQbk7ezxvasj4yAIsF4H7KBiZSGmWHkk4yADykWJSntTjFcM2wG0wSiEYoYzJQ'; config.TOKEN_EXPIRES_IN = 1515100500; -config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; + + +//config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; //bilal +config.SKILL_ID = 'amzn1.ask.skill.2445552d-954d-4cd6-b77f-295368e02842'; //saburly //config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server config.SKILL_DB_ID = '5a232fb86ce046c749739455'; //for local -config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7'; -config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3'; +//Bilal +//config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7'; +//config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3'; + +//Saburly +config.CLIENT_ID = 'amzn1.application-oa2-client.8c183daec15c488c9126b62bda9f7832'; +config.CLIENT_SECRET = '3acaa0755291132ee11e1cceaa100feef96a0244662df712a52189199cc655de'; module.exports = config; diff --git a/backend/config/constants.js b/backend/config/constants.js index 093f427..ef7c57f 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -1,43 +1,61 @@ const constants = {}; constants.amazonResultCodes = { - OK:200, - ACCEPTED:202, - BAD_REQUEST:400, - UNAUTHORIZED:401, - NOT_FOUND:404, - CONFLICT:409, - PAYLOAD_TOO_LARGE:413 -} + OK: 200, + ACCEPTED: 202, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + NOT_FOUND: 404, + CONFLICT: 409, + PAYLOAD_TOO_LARGE: 413, +}; constants.apiResultCodes = { - GENERIC_ERROR : -1, - OK:0, - AMAZON_ERROR:1, //amazon api works, but error is some of the amazonResultCodes - AMAZON_FAIL:2, //amazon api doesn't work - DATABASE_ERROR:3, - NO_SKILL:4, - INCONSISTENT_STATE:5, -} + GENERIC_ERROR: -1, + OK: 0, + AMAZON_ERROR: 1, //amazon api works, but error is some of the amazonResultCodes + AMAZON_FAIL: 2, //amazon api doesn't work + DATABASE_ERROR: 3, + NO_SKILL: 4, + INCONSISTENT_STATE: 5, + INVALID_SKILL: 6, +}; constants.HTTPResultCodes = { - INTERNAL_SERVER_ERROR : 500, -} + INTERNAL_SERVER_ERROR: 500, +}; constants.SKILL_ID_LENGTH = 24; constants.voiceResponseStrings = { - QUESTION_NOT_FOUND : 'Sorry, I didnt understand', - GENERIC_CONTINUE : 'Say something to continue', - DIDNT_ASK_ANYTHING : 'There was no question to answer to', -} + QUESTION_NOT_FOUND: 'Sorry, I didnt understand', + GENERIC_CONTINUE: 'Say something to continue', + DIDNT_ASK_ANYTHING: 'There was no question to answer to', +}; //Timing is given in [ms] constants.voiceResponseTimings = { - PAUSE_BETWEEN_QUESTIONS : 650, - PAUSE_AFTER_WELCOME_MESSAGE : 650, -} + PAUSE_BETWEEN_QUESTIONS: 650, + PAUSE_AFTER_WELCOME_MESSAGE: 650, +}; +constants.stringConstraints = { + INTENT_EXPLANATION_MAX_LENGTH: 70, + INTENT_NAME_MAX_LENGTH: 30, + INTENT_NAME_MIN_LENGTH: 2, -module.exports = constants; \ No newline at end of file + QUESTION_MAX_LENGTH: 150, + QUESTION_MIN_LENGTH: 2, + + ANSWER_MAX_LENGTH: 150, + ANSWER_MIN_LENGTH: 2, + + INVOCATION_NAME_MAX_LENGTH: 50, + INVOCATION_NAME_MIN_LENGTH: 2, + INVOCATION_ANSWER_MAX_LENGTH: 100, + + EMAIL_MAX_LENGTH: 100, +}; + +module.exports = constants; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index a0c2f5f..6ee4906 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -2,6 +2,7 @@ var express = require ('express'), router = express.Router (); const constants = require ('../config/constants'); var databaseHelper = require ('../helpers/database'); var amazonHelper = require ('../helpers/amazon'); +var skillValidator = require('../helpers/skillValidator'); var bodyParser = require ('body-parser'); var alexa = require ('../models/alexa'); @@ -31,6 +32,20 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { delete skill.updateOnAmazon; delete skill._id; + //Validate skill + if (!skillValidator.validateSkill(skill)){ + //skill not valid + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.INVALID_SKILL, + message: '', + }); + return; + } + //First get current skill from DB databaseHelper .getSkill (id) diff --git a/backend/helpers/skillValidator.js b/backend/helpers/skillValidator.js new file mode 100644 index 0000000..6d78cdf --- /dev/null +++ b/backend/helpers/skillValidator.js @@ -0,0 +1,100 @@ +const constants = require ('../config/constants'); + +validateEmail = function (email) { + if (email.length > constants.stringConstraints.EMAIL_MAX_LENGTH) return false; + let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return validEmailRegex.test (email); +}; + +validateIntentName = function (intentName) { + if ( + intentName.length < constants.stringConstraints.INTENT_NAME_MIN_LENGTH || + intentName.length > constants.stringConstraints.INTENT_NAME_MAX_LENGTH + ) + return false; + let validIntentNameRegex = /^[a-z]*$/i; + return validIntentNameRegex.test (intentName); +}; + +validateQuestion = function (question) { + if ( + question.length < constants.stringConstraints.QUESTION_MIN_LENGTH || + question.length > constants.stringConstraints.QUESTION_MAX_LENGTH + ) + return false; + let validQuestionNameRegex = /^[a-z,.' ]*$/i; + return validQuestionNameRegex.test (question); +}; + +validateAnswer = function (answer) { + if ( + answer.length < constants.stringConstraints.ANSWER_MIN_LENGTH || + answer.length > constants.stringConstraints.ANSWER_MAX_LENGTH + ) + return false; + let validAnswerRegex = /^[a-z,.' ]*$/i; + return validAnswerRegex.test (answer); +}; + +validateInvocationName = function (invocationName) { + if ( + invocationName.length < constants.stringConstraints.INVOCATION_NAME_MIN_LENGTH || + invocationName.length > constants.stringConstraints.INVOCATION_NAME_MAX_LENGTH + ) + return false; + let validInvocationNameRegex = /^[a-z,.' ]*$/i; + return validInvocationNameRegex.test (invocationName); +}; + +validateInvocationAnswer = function (invocationAnswer) { + if (invocationAnswer.length > constants.stringConstraints.INVOCATION_ANSWER_MAX_LENGTH) + return false; + let validInvocationAnswerRegex = /^[a-z,.' ]*$/i; + return validInvocationAnswerRegex.test (invocationAnswer); +}; + +validateIntentExplanation = function (explanation) { + if (explanation.length > constants.stringConstraints.INTENT_EXPLANATION_MAX_LENGTH) + return false; + let validExplanationRegex = /^[a-z,.' ]*$/i; + return validExplanationRegex.test (explanation); +}; + +module.exports = { + validateSkill: function (skill) { + try { + if ( + !validateEmail (skill.contactEmail) || + !validateInvocationName (skill.invocationName) || + !validateInvocationAnswer (skill.invocationAnswer) + ) + return false; + + for (let i = 0; i < skill.intents.length; i++) { + if (!validateIntentName (skill.intents[i].intentName)) return false; + if (!validateAnswer (skill.intents[i].answer)) return false; + + for (let j = 0; j < skill.intents.length; j++) { + if (i === j) continue; + if (skill.intents[i].intentName === skill.intents[j].intentName) + return false; + } + + for (let j = 0; j < skill.intents[i].questions.length; j++) { + if (!validateQuestion (skill.intents[i].questions[j])) return false; + + for (let k = 0; k < skill.intents[i].questions.length; k++) { + if (j === k) continue; + if (skill.intents[i].questions[j] === skill.intents[i].questions[k]) + return false; + } + } + } + + return true; + } catch (e) { + console.log ('Error : ' + e); + return false; + } + }, +}; diff --git a/web/src/App.js b/web/src/App.js index a91c5f6..f99b096 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -7,11 +7,16 @@ import LaunchRequest from './components/LaunchRequest'; import Contact from './components/Contact'; import Popup from 'react-popup'; import {getSkill, updateSkill} from './lib/api'; +import {isEmailValid} from './lib/helpers'; import { NEW_INTENT_SELECTED_INDEX, LAUNCH_REQUEST_SELECTED_INDEX, CONTACT_SELECTED_INDEX, RESULT_CODES, + INVOCATION_NAME_MIN_LENGTH, + INTENT_NAME_MIN_LENGTH, + QUESTION_MIN_LENGTH, + ANSWER_MIN_LENGTH, } from './config/constants'; class App extends Component { @@ -132,7 +137,6 @@ class App extends Component { this.setState ({ selectedIntent: selectedIntent, selectedIndex: index, - launchRequest: false, }); } @@ -145,6 +149,12 @@ class App extends Component { } handleSaveLaunchRequestClick (name, answer) { + + if (name.length < INVOCATION_NAME_MIN_LENGTH){ + Popup.alert ('Invocation name should be at least 2 characters long'); + return; + } + this.setState ({ waiting: true, invocationName: name, @@ -163,17 +173,21 @@ class App extends Component { } handleSaveEmailClick (email) { - this.setState ({waiting: true}); - this.sendSkill ( - this.state.allIntents, - true, - {contactEmail: email, waiting: false}, - {waiting: false}, - this.state.invocationName, - this.state.invocationAnswer, - email, - false - ); + if (isEmailValid(email)){ + this.setState ({waiting: true}); + this.sendSkill ( + this.state.allIntents, + true, + {contactEmail: email, waiting: false}, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + email, + false + ); + }else{ + Popup.alert ('Please enter valid email'); + } } handleDeleteIntentClick (selectedIntent) { @@ -216,14 +230,56 @@ class App extends Component { } handleSaveIntentClick (selectedIntent) { + + if (selectedIntent.intentName.length < INTENT_NAME_MIN_LENGTH){ + Popup.alert ('Question name should have at least 2 characters'); + return; + } + + if (selectedIntent.answer.length < ANSWER_MIN_LENGTH){ + Popup.alert('Answer should have at least 2 characters'); + return; + } + + for(let i=0;i { + let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + return validEmailRegex.test (email); +};