mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2025-12-19 05:47:46 +00:00
Feat/app env vars (#430)
* initial changes for env var support
* WIP
* Update applications.js
* JSON stringify before encrypting
* use call_hook.url
* env vars working
GET /v1/AppEnv?url=[URL] to trigger options request to URL and return app-schema
POST /v1/Applications with {env_vars: [OBJECT} to create app with env vars
PUT /v1/Applications/[SID] with {env_vars: [OBJECT} to change env vars
GET returns env vars
POST and PUT will also trigger an OPTIONS request to the call_hook url to get schema and then validate the env_vars against it
* update appenv cannot finish request.
* wip
* wip
* wip
* wip
---------
Co-authored-by: Dave Horton <daveh@beachdognet.com>
Co-authored-by: Quan HL <quan.luuhoang8@gmail.com>
Co-authored-by: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com>
This commit is contained in:
@@ -516,6 +516,7 @@ fallback_speech_synthesis_label VARCHAR(64),
|
||||
fallback_speech_recognizer_vendor VARCHAR(64),
|
||||
fallback_speech_recognizer_language VARCHAR(64),
|
||||
fallback_speech_recognizer_label VARCHAR(64),
|
||||
env_vars TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
record_all_calls BOOLEAN NOT NULL DEFAULT false,
|
||||
PRIMARY KEY (application_sid)
|
||||
@@ -743,4 +744,4 @@ ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (devic
|
||||
|
||||
ALTER TABLE accounts ADD FOREIGN KEY siprec_hook_sid_idxfk (siprec_hook_sid) REFERENCES applications (application_sid);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
@@ -2492,7 +2492,7 @@
|
||||
</location>
|
||||
<size>
|
||||
<width>345.00</width>
|
||||
<height>540.00</height>
|
||||
<height>560.00</height>
|
||||
</size>
|
||||
<zorder>0</zorder>
|
||||
<SQLField>
|
||||
@@ -2685,6 +2685,11 @@
|
||||
<notNull><![CDATA[0]]></notNull>
|
||||
<uid><![CDATA[65AA5173-6523-49F7-9D95-78C4B3A7C7E6]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[env_vars]]></name>
|
||||
<type><![CDATA[TEXT]]></type>
|
||||
<uid><![CDATA[C22DCA56-385D-45EE-A36F-2B9C6167AFAA]]></uid>
|
||||
</SQLField>
|
||||
<SQLField>
|
||||
<name><![CDATA[created_at]]></name>
|
||||
<type><![CDATA[DATETIME]]></type>
|
||||
@@ -3163,9 +3168,9 @@
|
||||
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
|
||||
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
|
||||
<windowHeight><![CDATA[1055.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[58.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[24.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 278}]]></windowScrollOrigin>
|
||||
<windowLocationX><![CDATA[1845.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[37.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 544}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1670.000000]]></windowWidth>
|
||||
</SQLDocumentInfo>
|
||||
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
|
||||
|
||||
@@ -225,6 +225,9 @@ const sql = {
|
||||
'ALTER TABLE google_custom_voices ADD COLUMN use_voice_cloning_key BOOLEAN DEFAULT false',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN dtmf_type ENUM(\'rfc2833\',\'tones\',\'info\') NOT NULL DEFAULT \'rfc2833\'',
|
||||
'ALTER TABLE voip_carriers ADD COLUMN outbound_sip_proxy VARCHAR(255)',
|
||||
],
|
||||
9004: [
|
||||
'ALTER TABLE applications ADD COLUMN env_vars TEXT',
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
47
lib/routes/api/appenv.js
Normal file
47
lib/routes/api/appenv.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const router = require('express').Router();
|
||||
const sysError = require('../error');
|
||||
const { fetchAppEnvSchema, validateAppEnvSchema } = require('../../utils/appenv_utils');
|
||||
|
||||
const URL = require('url').URL;
|
||||
|
||||
const isValidUrl = (s) => {
|
||||
const protocols = ['https:', 'http:', 'ws:', 'wss:'];
|
||||
try {
|
||||
const url = new URL(s);
|
||||
if (protocols.includes(url.protocol)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/* get appenv schema for endpoint */
|
||||
router.get('/', async(req, res) => {
|
||||
const logger = req.app.locals.logger;
|
||||
const url = req.query.url;
|
||||
if (!isValidUrl(url)) {
|
||||
sysError(logger, res, 'Invalid URL');
|
||||
} else {
|
||||
try {
|
||||
const appenv = await fetchAppEnvSchema(logger, url);
|
||||
if (appenv && validateAppEnvSchema(appenv)) {
|
||||
return res.status(200).json(appenv);
|
||||
} else if (appenv) {
|
||||
return res.status(400).json({
|
||||
msg: 'Invalid appenv schema',
|
||||
});
|
||||
} else {
|
||||
return res.status(204).end(); //No appenv returned from url, normal scenario
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -12,7 +12,8 @@ const preconditions = {
|
||||
'add': validateAdd,
|
||||
'update': validateUpdate
|
||||
};
|
||||
|
||||
const { fetchAppEnvSchema, validateAppEnvData } = require('../../utils/appenv_utils');
|
||||
const {decrypt, encrypt} = require('../../utils/encrypt-decrypt');
|
||||
|
||||
const validateRequest = async(req, account_sid) => {
|
||||
try {
|
||||
@@ -151,6 +152,16 @@ router.post('/', async(req, res) => {
|
||||
throw new DbErrorBadRequest(err);
|
||||
}
|
||||
}
|
||||
// validate env_vars data if required
|
||||
if (obj['env_vars']) {
|
||||
const appenvschema = await fetchAppEnvSchema(logger, req.body.call_hook.url);
|
||||
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
|
||||
if (errors) {
|
||||
throw new DbErrorBadRequest(errors);
|
||||
} else {
|
||||
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
|
||||
}
|
||||
}
|
||||
|
||||
const uuid = await Application.make(obj);
|
||||
res.status(201).json({sid: uuid});
|
||||
@@ -167,7 +178,15 @@ router.get('/', async(req, res) => {
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
const name = req.query.name;
|
||||
const results = await Application.retrieveAll(service_provider_sid, account_sid, name);
|
||||
res.status(200).json(results);
|
||||
const ret = results.map((a) => {
|
||||
if (a.env_vars) {
|
||||
a.env_vars = JSON.parse(decrypt(a.env_vars));
|
||||
return a;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
});
|
||||
res.status(200).json(ret);
|
||||
} catch (err) {
|
||||
sysError(logger, res, err);
|
||||
}
|
||||
@@ -183,6 +202,9 @@ router.get('/:sid', async(req, res) => {
|
||||
const results = await Application.retrieve(application_sid, service_provider_sid, account_sid);
|
||||
if (results.length === 0) return res.status(404).end();
|
||||
await validateRequest(req, results[0].account_sid);
|
||||
if (results[0].env_vars) {
|
||||
results[0].env_vars = JSON.parse(decrypt(results[0].env_vars));
|
||||
}
|
||||
return res.status(200).json(results[0]);
|
||||
}
|
||||
catch (err) {
|
||||
@@ -237,6 +259,8 @@ router.put('/:sid', async(req, res) => {
|
||||
try {
|
||||
const sid = parseApplicationSid(req);
|
||||
await validateUpdate(req, sid);
|
||||
const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null;
|
||||
const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null;
|
||||
|
||||
// create webhooks if provided
|
||||
const obj = Object.assign({}, req.body);
|
||||
@@ -268,6 +292,19 @@ router.put('/:sid', async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// validate env_vars data if required
|
||||
if (obj['env_vars']) {
|
||||
const applications = await Application.retrieve(sid, service_provider_sid, account_sid);
|
||||
const call_hook_url = req.body.call_hook ? req.body.call_hook : applications[0].call_hook.url;
|
||||
const appenvschema = await fetchAppEnvSchema(logger, call_hook_url);
|
||||
const errors = await validateAppEnvData(appenvschema, obj['env_vars']);
|
||||
if (errors) {
|
||||
throw new DbErrorBadRequest(errors);
|
||||
} else {
|
||||
obj['env_vars'] = encrypt(JSON.stringify(obj['env_vars']));
|
||||
}
|
||||
}
|
||||
|
||||
const rowsAffected = await Application.update(sid, obj);
|
||||
if (rowsAffected === 0) {
|
||||
return res.status(404).end();
|
||||
|
||||
@@ -39,6 +39,7 @@ api.use('/change-password', require('./change-password'));
|
||||
api.use('/ActivationCode', require('./activation-code'));
|
||||
api.use('/Availability', require('./availability'));
|
||||
api.use('/AccountTest', require('./account-test'));
|
||||
api.use('/AppEnv', require('./appenv'));
|
||||
//api.use('/Products', require('./products'));
|
||||
api.use('/Prices', require('./prices'));
|
||||
api.use('/StripeCustomerId', require('./stripe-customer-id'));
|
||||
|
||||
@@ -821,7 +821,6 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
@@ -837,7 +836,6 @@ router.get('/:sid/test', async(req, res) => {
|
||||
SpeechCredential.ttsTestResult(sid, true);
|
||||
} catch (err) {
|
||||
let reason = err.message;
|
||||
// if error is from bent, let get the body
|
||||
try {
|
||||
reason = await err.text();
|
||||
} catch {}
|
||||
|
||||
50
lib/utils/appenv_schemaSchema.json
Normal file
50
lib/utils/appenv_schemaSchema.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z0-9_]+$": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean"
|
||||
]
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"default": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"type": "array"
|
||||
},
|
||||
"obscure": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"description"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/utils/appenv_utils.js
Normal file
70
lib/utils/appenv_utils.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const Ajv = require('ajv');
|
||||
const assert = require('assert');
|
||||
|
||||
const ajv = new Ajv();
|
||||
const schemaSchema = require('./appenv_schemaSchema.json');
|
||||
|
||||
|
||||
const validateAppEnvSchema = (schema) => {
|
||||
const validate = ajv.compile(schemaSchema);
|
||||
return validate(schema);
|
||||
};
|
||||
|
||||
//Currently this request is not signed with the webhook secret as it is outside an account
|
||||
const fetchAppEnvSchema = async(logger, url) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
logger.info(`Failure to fetch app env schema ${response.status} ${response.statusText}`);
|
||||
return false;
|
||||
}
|
||||
const schema = await response.json();
|
||||
return schema;
|
||||
}
|
||||
catch (e) {
|
||||
logger.info(`Failure to fetch app env schema ${e}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const validateAppEnvData = async(schema, data) => {
|
||||
const schemaKeys = Object.keys(schema);
|
||||
const dataKeys = Object.keys(data);
|
||||
let errorMsg = false;
|
||||
// Check for required keys
|
||||
schemaKeys.forEach((k) => {
|
||||
if (schema[k].required) {
|
||||
if (!dataKeys.includes(k)) {
|
||||
errorMsg = `Missing required value env_vars.${k}`;
|
||||
console.log(errorMsg);
|
||||
}
|
||||
}
|
||||
});
|
||||
//Validate the values
|
||||
dataKeys.forEach((k) => {
|
||||
if (schemaKeys.includes(k)) {
|
||||
try {
|
||||
// Check value is correct type
|
||||
assert(typeof data[k] == schema[k].type);
|
||||
// if enum check value is valid
|
||||
if (schema[k].enum) {
|
||||
assert(schema[k].enum.includes(data[k]));
|
||||
}
|
||||
} catch (error) {
|
||||
errorMsg = `Invalid value/type for env_vars.${k}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
return errorMsg;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateAppEnvSchema,
|
||||
fetchAppEnvSchema,
|
||||
validateAppEnvData
|
||||
};
|
||||
8147
package-lock.json
generated
8147
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
||||
"@jambonz/time-series": "^0.2.8",
|
||||
"@jambonz/verb-specifications": "^0.0.72",
|
||||
"@soniox/soniox-node": "^1.2.2",
|
||||
"ajv": "^8.17.1",
|
||||
"argon2": "^0.40.1",
|
||||
"assemblyai": "^4.3.4",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
Reference in New Issue
Block a user