From ed51d8b13f2bddd0706cd8bc046f1037125cd2f5 Mon Sep 17 00:00:00 2001 From: Dave Horton Date: Thu, 17 Jun 2021 15:56:21 -0400 Subject: [PATCH] merge of features from hosted branch (#7) major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org --- .github/workflows/{npm-publish.yml => ci.yml} | 2 +- app.js | 56 +- data/test_audio.wav | Bin 0 -> 28648 bytes db/add-predefined-carriers.sql | 52 + db/create-default-account.sql | 4 +- db/jambones-sql.sql | 379 ++- db/jambones.sqs | 1309 ++++++++++- db/reset_admin_password.js | 95 +- db/seed-integration-test.sql | 102 + db/seed-production-database-open-source.sql | 101 + db/seed-production-database.sql | 78 + db/webapp-tests.sql | 105 + lib/auth/index.js | 121 +- lib/db/index.js | 4 +- lib/db/mysql.js | 3 +- lib/db/pool.js | 10 + lib/models/account.js | 176 ++ lib/models/phone-number.js | 39 +- lib/models/predefined-carrier.js | 63 + lib/models/product.js | 28 + lib/models/sip-gateway.js | 13 + lib/models/smpp.js | 55 + lib/models/smpp_gateway.js | 60 + lib/models/speech-credential.js | 92 + lib/models/voip-carrier.js | 63 +- lib/routes/api/account-test.js | 57 + lib/routes/api/accounts.js | 176 +- lib/routes/api/activation-code.js | 117 + lib/routes/api/add-from-predefined-carrier.js | 71 + lib/routes/api/alerts.js | 35 + lib/routes/api/api-keys.js | 2 +- lib/routes/api/applications.js | 17 +- lib/routes/api/availability.js | 31 + lib/routes/api/beta-invite-codes.js | 32 + lib/routes/api/change-password.js | 47 + lib/routes/api/charges.js | 46 + lib/routes/api/decorate.js | 10 +- lib/routes/api/forgot-password.js | 82 + lib/routes/api/index.js | 31 +- lib/routes/api/invite-codes.js | 34 + lib/routes/api/invoices.js | 26 + lib/routes/api/login.js | 25 +- lib/routes/api/logout.js | 23 + lib/routes/api/phone-numbers.js | 30 +- lib/routes/api/predefined-carriers.js | 7 + lib/routes/api/prices.js | 108 + lib/routes/api/products.js | 78 + lib/routes/api/recent-calls.js | 37 + lib/routes/api/register.js | 344 +++ lib/routes/api/sbcs.js | 15 +- lib/routes/api/service-providers.js | 48 +- lib/routes/api/signin.js | 85 + lib/routes/api/sip-gateways.js | 49 +- lib/routes/api/sip-realm.js | 67 + lib/routes/api/smpp-gateways.js | 53 + lib/routes/api/smpps.js | 30 + lib/routes/api/sms-inbound.js | 2 +- lib/routes/api/sms-outbound.js | 2 +- lib/routes/api/speech-credentials.js | 248 ++ lib/routes/api/stripe-customer-id.js | 48 + lib/routes/api/subscriptions.js | 334 +++ lib/routes/api/users.js | 246 +- lib/routes/api/utils.js | 178 ++ lib/routes/api/voip-carriers.js | 82 +- lib/routes/api/webhooks.js | 21 + lib/routes/error.js | 24 + lib/routes/index.js | 2 + lib/routes/stripe/index.js | 5 + lib/routes/stripe/webhook.js | 71 + lib/swagger/swagger.yaml | 2074 ++++++++++++++++- lib/utils/dns-utils.js | 123 + lib/utils/email-utils.js | 34 + lib/utils/encrypt-decrypt.js | 29 + lib/utils/errors.js | 9 +- lib/utils/free_plans.json | 22 + lib/utils/oauth-utils.js | 112 + lib/utils/password-utils.js | 24 + lib/utils/phone-number-utils.js | 22 + lib/utils/speech-utils.js | 60 + lib/utils/stripe-utils.js | 224 ++ package.json | 33 +- test.out | 998 -------- test/accounts.js | 83 +- test/applications.js | 2 +- test/auth.js | 36 +- test/create-test-db.js | 14 +- test/data/subscription.json | 908 ++++++++ test/data/test.json | 12 + test/docker_start.js | 5 +- test/docker_stop.js | 2 +- test/index.js | 4 + test/ms-teams.js | 2 +- test/oauth/gh-get-user.js | 19 + test/oauth/simple_test/index.html | 14 + test/phone-numbers.js | 59 +- test/recent-calls.js | 79 + test/sbcs.js | 27 +- test/serve-integration.js | 25 +- test/service-providers.js | 14 +- test/sip-gateways.js | 7 +- test/smpp-gateways.js | 88 + test/speech-credentials.js | 123 + test/utils.js | 18 +- test/voip-carriers.js | 28 +- test/webapp_tests.js | 512 ++++ 105 files changed, 10330 insertions(+), 1601 deletions(-) rename .github/workflows/{npm-publish.yml => ci.yml} (90%) create mode 100644 data/test_audio.wav create mode 100644 db/add-predefined-carriers.sql create mode 100644 db/seed-integration-test.sql create mode 100644 db/seed-production-database-open-source.sql create mode 100644 db/seed-production-database.sql create mode 100644 db/webapp-tests.sql create mode 100644 lib/db/pool.js create mode 100644 lib/models/predefined-carrier.js create mode 100644 lib/models/product.js create mode 100644 lib/models/smpp.js create mode 100644 lib/models/smpp_gateway.js create mode 100644 lib/models/speech-credential.js create mode 100644 lib/routes/api/account-test.js create mode 100644 lib/routes/api/activation-code.js create mode 100644 lib/routes/api/add-from-predefined-carrier.js create mode 100644 lib/routes/api/alerts.js create mode 100644 lib/routes/api/availability.js create mode 100644 lib/routes/api/beta-invite-codes.js create mode 100644 lib/routes/api/change-password.js create mode 100644 lib/routes/api/charges.js create mode 100644 lib/routes/api/forgot-password.js create mode 100644 lib/routes/api/invite-codes.js create mode 100644 lib/routes/api/invoices.js create mode 100644 lib/routes/api/logout.js create mode 100644 lib/routes/api/predefined-carriers.js create mode 100644 lib/routes/api/prices.js create mode 100644 lib/routes/api/products.js create mode 100644 lib/routes/api/recent-calls.js create mode 100644 lib/routes/api/register.js create mode 100644 lib/routes/api/signin.js create mode 100644 lib/routes/api/sip-realm.js create mode 100644 lib/routes/api/smpp-gateways.js create mode 100644 lib/routes/api/smpps.js create mode 100644 lib/routes/api/speech-credentials.js create mode 100644 lib/routes/api/stripe-customer-id.js create mode 100644 lib/routes/api/subscriptions.js create mode 100644 lib/routes/api/utils.js create mode 100644 lib/routes/api/webhooks.js create mode 100644 lib/routes/error.js create mode 100644 lib/routes/stripe/index.js create mode 100644 lib/routes/stripe/webhook.js create mode 100644 lib/utils/dns-utils.js create mode 100644 lib/utils/email-utils.js create mode 100644 lib/utils/encrypt-decrypt.js create mode 100644 lib/utils/free_plans.json create mode 100644 lib/utils/oauth-utils.js create mode 100644 lib/utils/password-utils.js create mode 100644 lib/utils/phone-number-utils.js create mode 100644 lib/utils/speech-utils.js create mode 100644 lib/utils/stripe-utils.js delete mode 100644 test.out create mode 100644 test/data/subscription.json create mode 100644 test/data/test.json create mode 100644 test/oauth/gh-get-user.js create mode 100644 test/oauth/simple_test/index.html create mode 100644 test/recent-calls.js create mode 100644 test/smpp-gateways.js create mode 100644 test/speech-credentials.js create mode 100644 test/webapp_tests.js diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/ci.yml similarity index 90% rename from .github/workflows/npm-publish.yml rename to .github/workflows/ci.yml index c8d315c..4588822 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - run: npm install - run: npm run jslint - run: npm test diff --git a/app.js b/app.js index 2272897..10a159c 100644 --- a/app.js +++ b/app.js @@ -11,7 +11,6 @@ const express = require('express'); const app = express(); const cors = require('cors'); const passport = require('passport'); -const authStrategy = require('./lib/auth')(logger); const routes = require('./lib/routes'); assert.ok(process.env.JAMBONES_MYSQL_HOST && @@ -19,13 +18,19 @@ assert.ok(process.env.JAMBONES_MYSQL_HOST && process.env.JAMBONES_MYSQL_PASSWORD && process.env.JAMBONES_MYSQL_DATABASE, 'missing JAMBONES_MYSQL_XXX env vars'); assert.ok(process.env.JAMBONES_REDIS_HOST, 'missing JAMBONES_REDIS_HOST env var'); - +assert.ok(process.env.JAMBONES_TIME_SERIES_HOST, 'missing JAMBONES_TIME_SERIES_HOST env var'); +const {queryCdrs, queryAlerts, writeCdrs, writeAlerts, AlertType} = require('@jambonz/time-series')( + logger, process.env.JAMBONES_TIME_SERIES_HOST +); const { retrieveCall, deleteCall, listCalls, purgeCalls, - retrieveSet + retrieveSet, + addKey, + retrieveKey, + deleteKey } = require('@jambonz/realtimedb-helpers')({ host: process.env.JAMBONES_REDIS_HOST || 'localhost', port: process.env.JAMBONES_REDIS_PORT || 6379 @@ -34,7 +39,10 @@ const { lookupAppBySid, lookupAccountBySid, lookupAccountByPhoneNumber, - lookupAppByPhoneNumber + lookupAppByPhoneNumber, + lookupCarrierBySid, + lookupSipGatewayBySid, + lookupSmppGatewayBySid } = require('@jambonz/db-helpers')({ host: process.env.JAMBONES_MYSQL_HOST, user: process.env.JAMBONES_MYSQL_USER, @@ -44,22 +52,35 @@ const { connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10 }, logger); const PORT = process.env.HTTP_PORT || 3000; +const authStrategy = require('./lib/auth')(logger, retrieveKey); passport.use(authStrategy); app.locals = app.locals || {}; -Object.assign(app.locals, { +app.locals = { + ...app.locals, logger, retrieveCall, deleteCall, listCalls, purgeCalls, retrieveSet, + addKey, + retrieveKey, + deleteKey, lookupAppBySid, lookupAccountBySid, lookupAccountByPhoneNumber, - lookupAppByPhoneNumber -}); + lookupAppByPhoneNumber, + lookupCarrierBySid, + lookupSipGatewayBySid, + lookupSmppGatewayBySid, + queryCdrs, + queryAlerts, + writeCdrs, + writeAlerts, + AlertType +}; const unless = (paths, middleware) => { return (req, res, next) => { @@ -69,13 +90,20 @@ const unless = (paths, middleware) => { }; app.use(cors()); -app.use(express.urlencoded({ - extended: true -})); -app.use(express.json()); -app.use('/v1', unless(['/login', '/Users', '/messaging', '/outboundSMS'], passport.authenticate('bearer', { - session: false -}))); +app.use(express.urlencoded({extended: true})); +app.use(unless(['/stripe'], express.json())); +app.use('/v1', unless( + [ + '/register', + '/forgot-password', + '/signin', + '/login', + '/messaging', + '/outboundSMS', + '/AccountTest', + '/InviteCodes', + '/PredefinedCarriers' + ], passport.authenticate('bearer', {session: false}))); app.use('/', routes); app.use((err, req, res, next) => { logger.error(err, 'burped error'); diff --git a/data/test_audio.wav b/data/test_audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..825def1adc851620479f1869a207e9252aeebbad GIT binary patch literal 28648 zcmW(-1$Y!m({35roz-|q2qeJ?4#C~s4|jKWIPBm!oWu1FcRk$QgS&*d>-vs$|NZ^X zKD(RQneMKxuC6Y5D?K~3ZJRt5fW9sIv>7vbR@LTMe zUGxCmN3YPw?Dd{HC<5W2JctH0KvU2d^Z=dN_Y5!#3<5*g)etZRi~#LG2hb062QAr? zk)SOI0^cYAEkFq%vFj?JHhVV%tw1Bzb}*>O{+b7Rg68bHKj^~lg#iv|RAXc1P%kwA znSQ1U&7$9_2n3KxIZzsyKpqVRO;}$QK?BeWlwzY#X1xla0vo9*8@&UgFBnu~<5yvK z?H~pi*>^=y3xog<#jKAupdRQ3eq&G62BR1a0~z)G*;P;WYacL<-Bs9#r9fTwWI5L6 z1I?jkP!*Vg$gW#5ddsn^PGC5Db_)BP4~~O_U=LUewt{`^{WsVKcChPZ>~0U%V;fM1 z&5Q#9j0Q7!NeL|t44@Jla}nd<1jdW;tdD+R0aywqvFkysOiK2*&kC^nX-5r7zjo zG7SN#j2?wXG5O&boq4n(_?<~b6V^j3_G-eY3jO~T4`S;O1%9%1%%MlSpp$AH*{W7!A6zy)_fmWKvw0ai}a?OCNp6o=c-A7#-bdPdb2o)}$@y zHa3sjY<=$07`7wg+33NbB$MrhY&C{}sjR17U^W=T_OOku_#h?=KiF8w^eny3+V!KM zRG@WfAU#fck<%oaR-}z+9a@F)t{Ib`DolzGfEYLsmWEbnf{WozI1BRdGpG(1!R7D= z8*?dm43>i)bT}~+s#nuPwHYc<%c(hPh}Kc7q^~52v<=LL|DkN0zzyIh@h$l!d;%XL zOcvsWAN(EucRrX`u@2YL_IeHVxZmYD;Yx8tIb&SVJ<)T@w_7PqZh%H88AtNt_>Fuo zUg6I0M&YE;)NsdG$<)jI*!+(<#8Sx|BozqvU~B!S|A6alfhlKx=GV-x*%k9v6yA5O zP(+x+|7R#GSxm2`qlW7ogbt#S5#S1n=BJ3yO-pR21Gu13K~DPvYYFoQ@hNmF*Inxi z?q?lNElR1MRwHv$UJ1t|9|y(=7md?Qfu?oF?&1n`Sx=MO`C9p|`kSi&4iPt)?**oX ztO+9}a!XtZ{Tley>=Ns9JM<~OPYxl^o~5O4Pg|8K<#s5H^-NXE!}~%dV@2aO!)q>^ zuF-OR+3we_5w1*6RpkjuC2Q3V57129FjhGqnB&=4@E{j|Eh3`?5UP6g+#TG2c zPEI?J**T|g_UrtWjw`;+;6B%hUno8k1il#kRQGyUdzQNePh;ShFU`H zKSLgr=nxTC;zn3OSV(AH`y0bk!TG;^%+XOJ~8 z12^PLMv`}g7z2qbXZE^=^T}~UCUNU1^X1Tns#Vx!=+HklF2M9g*x*S3KX)PUU&y=(3 zb$u}n=UN-$%$o!BkUvVakNi8rU7}5Rwcts1lc_XkflamV@&qN$``qnv&dooT(;%yE zh9&E2=F@C{!Fksm1w(9QjN8U8|zcB4^MWKGJyFvLYZaC@EA8 z{SGg zqYEe&TTyLl(E6(V)pj~ZZjfG}EqB4t*@EpG0=W=hNPJjKSVmA;dwFwbu`Ab*yis!f zbNtx1z&qNr&-K*#Dt}AvS65BvEmt}3D$g-(GhU5d=r$~+R8UoAzHg}SmwSdML%pe6 zKq~i!zi-M5Xc~AbsB-A4U}H$7kk$4mTYF1Q!x143H>c}C09~qg(x+cul2X@O~{pmTT74VnvTkTPmzRrh(_d9I0$>qQ~O{=%3drN~>f z-$~pLJ*9lT{QLdO z_P77E+%+c~8%e(#M+tTKvPgh)VFP+YtDw2%tG*WQCq+{WZsfPgOU<2^`!Ua4+`{ei zew7EPqje)0qXuguwUSz8(g`NRBHU3pXt*hbNiC%<=H8~VrrFX@>9Ogod6enADc;=B zY`5L9G&Zd>|7MsiOy=@oWqqtVP+q~b&sZkY7d;1EimRThs-VWm(_u!$Fi@lv+2g=vu`);!MA(Av?w$9&!xD@7P0xi5Sj z^c;!k3fGHnVdv;E@L11OU-^eAN%A27DOr}Us>4X2UPliDm0<()k#^G8>JF`h-ctSL ztLcwZ`m0{`llF^Nf>CH0TnFaBCujv)j{Bi|*oo?Jql|5YBjQ2BN7FIm6e(OdZa5}% zFz|dw!)er*FU4(wcVH@NgHlif`U1`cPp}8xqpN@dJ#ZVG435LPcqn%qrNIPrfcp(i z0vACs8UsI~ao|220(+CCbe`_g5cymCPY=`j=-bF$y*G+y&&8sxcpCZ$-jTt0CaMTE z{UW{0PMFu^7HS6P;5z6tO#)l>BGOyGq3%(ulX*0Vo#CfRKAl6OX>E%1n%XIKx%Poj ze?|40enfqxZPvOdC$)KMjJDZVPaY|Mk~_!^e1FI;^|`k^=_&70yQnGhAL>NkDlhR| z@T9v3yG`y6o>oc?_cwpGue?9WcTt}2H~7Z;SNj`#_sIr%f!1G{s3J8&7PP(UO7C;^ zqyMa?_!}uce}Z~Y7FDO$Bo`}ZeBJ#+{8!};KE<6UulCLN-0>uNkGX2QK0A+kGF{g_ zy`0tD&|TX#$z^t5b?11#cpE8?yplFRi_zApC*>T{Q@g5e)ov)`Nuk<}*@mx)A56o2 zaU2*7d*f`>7`MW|f!+8|VFFOOHQ+d2hXHKO?c}p@688t+6({k38GaWY8JZhY#fC<+ zajRjyp^ntWw8WewMVoe;dYJc{??`?_Nz-A&cSA#Cu%UwSPbu0`-?B$4GCL)Q^tXAS zbVE8Itv5Z9b{kijPMU_8n@Y(>7uyLx%r;X+6E+u{df2|Py>Z^`woEo3kY<^p%^9Za z<`i=~a~0bSbE>(s^@6p?RAf1Ckqk$rcaqN-Yb4?@L!3ApU&qU_3n!uZs2?7Qb5L_U ziR*w%<3GV{{0~||FM(Igf~VvLvsB806lTfH2fe{|@R$t8CU^oICAVk|!jYR~EqScj zRU4K4SCu4kQp;A?YxUI}<%~Q>_sS=9sO*r>D@BT?cUJ4GVQLMngmPWktt``?&a)NtZaK51Okvv_Nv>^(S z-z&?MLn>CD__wK0>!Ai||Ee{0q-~RrssAV=lovixd9QYrla(%dPqLhb>B)LaY9>1P zsPBi9n0=TI4w9MR4*U#Tf}Su5)3l9EH3VGlTZW)S3 zm(Vk^9^59=X{xqIX|6}=LHYx=wi>S=QyXi;w4?GaWv%S@cl4c+d#VXcmN)yl$+7Zr zuh(ak*DC!K#VgCL)l|PxPgDM2ye(9I_>OD6<#t-CT2?OU73KT#Ywvf@Mt>dovhShn z)fV}8lX+@{K23M2$LJ7!o!W#n)2Hh@wPB>E{*_tEec>Tk6Rtfhm*uI!cwjcAIZ77 z8-krn7EbWXjYeZOu5Jk6QjK+mc(IJ3GH(-?a2a?xzf@=?{=o-vfAKk7eW4FO4i7RE zaPQC>a0yuo33C$b44_@ZzZ^%K(JY;5H& z;Q1U7PT^SC9}mY9>0+)5XoBkj5x0d19iwYuE!c(Dg)uOPyhO+GGuQ}i1A9I2FG7qRM9S<&m z2Xq_wT@QrM!AX(<-qQYb7wc<3u%Wl)EE$jb!gu;rE|lD3E1wB!qBhKviKlf*d+h~% zt;cJP^>W%guBr$@(=US`q2F(mz~-V zh=f|xMc_F3Mcy;d=p^uo(^54F);Pzd zp@^ajoMb4bi(wTZPK@PJKt9Z6{hXqia5?IV$MfN|8~6jSLmgoSJP{>xozNffBIp9o zpyuERD2A2ja~38{Yf=?V&^E(JdRALPI@1uX zKD{GH(#m9$JVhDYb^56C~RJzFX{J-45Kf+&6z9X0NbyeHT!xd3} z*?(3?rrPt=SRL~-o5^BF1N4P^V40=v)}cXM{~z} zesh=d?sj)@C%9uhW!MRS&wbPN-1EZO(D%eut>wybx8*HIL&x1VnD}e=eA$WLD6Z^m*F(4wy84wcq zBY*@v3rM$jvp2Pytha3gEx-aTYmL|(Y|1lU6P$+c;!Q(KVHZDxj}*PIDc*+Wpv$-> zcn6k~ub2lza1NaYFTe-rDmhNtz~!Jdysc5Pmsun?XdM#7;#j9(Q+h@3$WE#p+F!3p z?(5ZP8J*KEYBNa>y}xSGoa#n)(xj?Glt}HM+EzQN7AgDHd-@5jSeva&`a-6+e`=4l z6C_hV%k+7q-iGFbl5{Y=Mr*=lXf@nPTSF^Kg?~dQn8~ct7~Bl)!ZF++j^kT#zwkd? zA@hG8;##-{UV;bUA)FgG<~ng!eh!|DSD|7w3CplI`~a80R?th=F?;eVtqM-ipL9Np zat))~$s;mZkI<0zhcZ>sl&8wSYB?o9X|5#8yi!%^%)Amsec@~8E!O|=9P$0;o8a8x zigZ76t#tNqH+5}vPxJKhbaW@VJ~^}9)7`^eS*~+Vv-<|~px1by9H5+5!kFh7B+v5^ z*O21zg==!V=8VZ&k~uJ|e9ob~qQW|kHqI{ISUFa?tz^m1<(0~CB~xyunAItyJgO}m zHE4!^jL)Sc>5ABzJ4`~9ioW3<$=%u+=sM@p++RI@?*QKf-!CSmFWq_W|2#g=G2awr zrCtKGz+3oNhH~aC`@oQK;iDtAm+TasS?XTon8^7hj)bnT5uR!tJ@xZjCT{sUAh~(Q zZ^ez|v-EHClCTCPOGSX-ICG+qtgOpFmL(Uo^!))eew^ubuq$F?RPT~~!k>iQDsd)! zN4P6&TvF7c3DsKtB}ju z))(tmEr(Y%;LRsM?I5jX?%@nz-j$mslS%G>3vJ;1!mH0)zDrH(>6~e@b074ddOOn1`(MGr?=Ro9{LxDMQ0YrRepuZYP%gi8#S%v(gYZK(SC@3|5ba@ zJo#4PdZ`+g9JPv~ip#s+IjW{idYk-iXKEGow)wks(LNyjYFKOu-muR#vbdtWNSqb$ zD4?h9V9@9i55f-zTNp1E;C-frVHx2s1NZ0md+_W_eC8tj(|XyNQ7jeqay0Xjyy;(i zy?>JwRhR_^S!xAjgqTBL2OlzD(I$I7ySM8tr1$nE!S_Sog%*VF3)m%f(P{VFhdqqi{lFEY|`AUi6vs-tG^`^30U_r6xnR`hfB10_-;$Atc}kFXep zYg(>nrLvdXV#ndLLw^esLi2(y1s#+s8Y@_91)q)hEo#1P*!Mn9%wOge%m|+;&G2u{ z{g%iV#uV1bu_kNZ!_#{y{-7^WFJemC{t)^ZsbPgUO!dnB&^t?;5_RZ`lZ~-yI(-_2BoY{QS&$Q{_qRY zTS~~5&k{0KvfSh2KmxqPN7@1c6YXbh^-Rfz&c@!x`Su|ZPhvM!1Z6)M9=yGL|5QR$ z;gf)yz~=w;YwGtl*)MXQ6)w(*O)+GN>a&2Q(fvz(vK|)?a0AT!O*$qpjr0_I+Ce~W zKv(lQaf-ns)Hn6BZ4VtDbGXd9NPj`MXZbH1B-*5ol0iG1Tk2!Sq=NLB1@;0dvsT8) z;xXczh@r6+gU)e(lKQxUDTCwTFy_fLF~!?60!G?e8Rr=98D!H#Ywh6FQaj3P<$94T zul483mnXv1fEi$3LBqGBzAC@Q<*Y8~n_fBNQNeqDL&UG>wZWsfE7}x1%91F3#mCjs zS~JdMHrR_z%fxA^oKh=&+$@z$b}vygXbU2EG~ITfbVS8{cwa!ZRVooE$VO z`dlSaVNa?0`467PCPe;vV47(#$W_yA?{EsA^V;zw?`l%5Bsu>YH#Xw;n23-w+!@eF zDh!-!dx5X{+IV-6WJ4=!wz(-^1`dWV#8v^PLkC4W%7Aiu@W9k7&r=hQXTP(C;8_3g zUsXT1%e_{5UA}mJEBlQd{AACGQk6IDaD{MpYveyq@ z1*9$zIvZ8x&#YbF{aL*V9u+-IKl!6x+75r~;5udgEj`jw3{QgtW|z$&?pK#NKltzC zp?0Tjk6|-yr$5DCjZ*^Z*sli_hQ>s!k6B}Gls4k|0Y)s0&I-PP@6a)(z9G@}AZjY!<9rMz*&106!;ZcrZy-O-b~b2*?Uw0Y zKt##273){bkv9IU^d|3nmioJeQxD~3eL0X)T-e@OT+k(Tdipuf5vf+hr>OXVj#5=_ zjj@GwCs#&EC~oEd%-xsXaCiMD-Q!6I>)WtF;fBDQK?5VtR?4e2zU1xPdCyiPR4-l{ zkSPi7>ff(_4$k@Eh$$YBtbTovHJG0oJ)+X*=v~3#majrx?y6khz08f2OXv)K0AhXT z9BW;bv;!!T?{o;NOh|Odir^U$ zU&|-d$*Ub-rF7V%?75F>zU-P93ft&A3qIsTr+4(Na@n%c&y2(*Z+7Ui9#`o1bnDt#)C%D;|D^bdWw z^v3t6L7rPigF3#*nk3{^^9A{Y!W|i-3Q8G%mE9CKw@jfG!0&nsrM!Pu&h(6WxuR=? zo~&(=TlwtD6V%KKgW`g#g~vu~l?^p!)EHCkQ-#{4k!$en%p3VHR(od)i(HQi-lo2C zj8Z#0cBG$9Je`dVr_0Z;{-Hu%gO6Y^# z#+Q)^Rqxle);w6gPI-6bDsfk%ces~d8+f+!tz+v$xFsvL|DP*~8MVp4xt5{L?N)qTz`nD|2tgOV1XL_);=Erj&E{3T#_>SB16V zC(P@N#nJ+BJAcRf-EZ4w+);M&ZPh5Z=HJIJvFotznmRiVE5kgO;%fT zugo`t{tY@Q_2(M#)vaGjBt<`no*Ol>ik&! zwJX8vj1Z{V>rz}6KY_$0-2|{^p1@E@b z58D$)!jDEiDcwD0XV?sLDd7>C$J=-g7LxIvKXVWMZ2tLEYEJGKN4y)mrt2Fy1NT%b zDB6;nU3kjB5lxhu+WQ6l8@S7`MZf2E7Hr7$BzMZ*Dx0lJiB(}=tRFZxh=Oa;VKLbD zZ`hK^uaW;o{)((tVt7C|=`hy`P9~gxOtCAg=*R6JXEWav#JE>@a@`Zv#pIjbOup?} z;jKY83o%j&(`8etWuW;JS6K^l3`wt%9qnl9UaGCtPlLPC9^*4%IaeK+4W$DP29=M{ zqRge<$GnKF892iFNqi?ba1(92lHwofiODo{8{uqygSVVJ!uct0ZdS9@(%G@zZc23)4}Ig$WO4a9o=wH49i9Ed zz!UgHU#%~rlcAlwa_=tcS8&z!UBAUL$8Uiw4iAbC8D^hi?ql}bD+hZ6TG-ZE+uGU$ z^s*(GMp{POZkl!6Re9i@;O*tR=>OrF?buWJU-3_Gf@?>9x!kUK!(C1M?|n1(JldROEo*LUkc8`onx+KfKVm7vW%JG; zK4iRosyW^?!c%>mcMr~wPg7HqvySF1F1%IT#@WEV&|TW)asJ`D;!g1H zksr`FBd~LU_biD*Uz9*Nt(iWVlqTarN8D2EX)G)Lhg-w8U;+9UzeNv_ju(n!EENJG zgPsRIw!5vvEPG7PjID$ds3cm57JyRPG+${~MqYN>_Fv&Y>CZ-~L$XTbH7pw89N}%P zHUO>p>PF6ttk11ytt+gOwTvaon8U4Qxts^+GlO(RF&gY&P%32=Sdj!1- zycdvSA7a;Rr>!}bkETn;wPG?{qog@b=bq0@PraIQC$&>*gVbv28Ci?+=M`OXINdK4 z3OgEhnaWspS{vKH*n3*fn8KvXh8E&FzB#H(g0<&bby^W-u?+t__zS(~+6wQ5CI(6R z%`(Rx8Bix+rM;{5mDEnSjgHdJY8{{GK3sG^=V->TjJS+V>ATV|q%X*dD46LQ}`G}1(@$kTf~0+HkOUP$yZ^~?n8u-m9#HyL1vLbU{|s}olU(>{Zr-Y zYJa^lsKhd0f?>X8GfN(9Hdi-}5Ci$;a4JdDUxR&k9(M}|qr)sJvk&crN8uTKOgw1X zZ#iilYu_B09W*HDusy{bE%g?+g0=oPo^V$}!Q}igIo;A~q9%y*Jj^=MGS6f%))%A2II)1+%+`FpZqbYNUt}$uj|DD>?O+;ChC*WkJd zJH!BEe{&OSO?$P#b@qQP(b5$DE{g;uQ{*4xiuCZVT?J8jVVO^oGm>#yi_ALt6&OEr zm8Wz(cR=iI7-U#t*eKPoUb9h4Yx6VX3n7`?4sWt(%6H|kQctT-0$>1_#`hGmgj}&u zENeJmENePv&a}?8-?x{u=UY}87ja+ET=-s@#ImUQ1+J{|S;ovMX*<&DWNge{P_Upl z)zQ?mKsgTz`96kP#*b2{<)HPx<*78+P))2Zo@UwiO!$*waUvMP<^}i#+Q7O{L_6_y zt`48b9~NxJv*v+T*@7*@%_~hw#%$ph-iSg`6xr%KI@B32s)xVco=#C zvsu&>i9?K=O%u##%)2cIEkn#bqz_^lz8Nln$7xeVcZays9FF{Ed5v;=WKGNLlJ#58 zmsE2ZOAB*p(*oldF;4Jv)$u&Ii~97X`a69(=|mraT$WGb zaXW4xUo4z7?3Ff~Us-tDT-!=(8%v}qR@9(L&yZ*Oy0{J$p357Y^ET^tR_*NGIpgwH z7OW^f;5nymf}eSp_{?z1ctUz0^=ErFQgrkGlfhHsc%cT@5r(o%&IJ9UevvEzDm37Y zTovK6xYJ-VZZ$5GWYb=jWqe?IBb_n~hs$O11=OxrP>*9$rhG=={n7ygf@ zY^`rdKGS~lbBk=dzFgAa8&VSji?6*0U^TdLcMIW7meZAEd zL;(l!dZEyOq_dJN{UdEQS`86GYZhmJ2GcNUyWoIe`rxF*Iw^RxvYK%n7M2r z+<4yj*to^`kKwkVhqTo+U&<68@z=P=xFwSm3oHr0GnC*&ph2Bw|LU+@%`<+nxX(~z zcqzr3*K_Z=_ApVNDvNf=j8L+y{P84KBok_;7EG1 zXQ;b_@2EUO{-rF^%jpNS_oN1$&I?I=4L%1SMN{E^a0%RCNWDq$JvziK z7pgK;#7g5>LsxO3@RKXS%|=gXGFh+BR2s`B`L1uUcai6n=c#v`x3zDKJV~jqv{#0z znOZM$n+Bog+%Y~uXk@rZsSVZ5+TYqBb(Q*D ztEbPqy#7rcfze`Jnn>Jkr{S_VK9-VlMW1dIFq3i`@*v@5ISH6dWJfp zZs;+31nTGoWCpFHL@VR;M%r{GMcqLbf((WOECYYQKw5?LW5~b|(p(=vHqqUrKD~&x zaVzm6e4j&nJFXY5%01%_<2W40mlfLb`?f|9FjWL9phzDr-nR{tRF!TNa) z#)1I26wN|0Xb?hpE53#wbNhKO@8kaAo^sD{BQzS$U|G@j`e^NnDk)d}kNlzX34aB- zi&9CgqwUfkvz-1)mRYZe9jjixz_3 zuvuKl+#Yjxy{{jK8(4%YkYiZ6M1w@1tv2u6`lQ7(Ia1&<%JVhM@^8&-Nah z_@R85_+2O?PBcavs~cAu=NRVm0=EjD)DFsj`>wjm7FTl&bQBgfbRO^w^bb>u$w7D# zFXZmvZ0-uz4NhffkAgXD~x0^D5>qdn>~fDOU^$XbBks;CcBTj&Uo%B=~^Ty z1taljZWGrS|6EWow!IzI2Tvo4vA3eB&C_hThq+T%@ZuEqS$ZRWw;Eh+$KJc8-*6aNA$gZQ(dnNQ8%j{w99%SxQjx0#n6x8^Ge(E zY^|pxt!<;-!FO;2 z&g9-fk^D_slUH&yL#m8r8MixX8}bdd=QQ483b8J=cCglk_D8_q_PwSuhPQ@0 zd=C%@nKsmZsB>hGx4@I-Jm+w`WJiKN>{j%m|_ODZ! ztW8!vXup9!0l+uKXlbJPn5~|@d%)Ddl)&DBIN*2tILm73goK5vkVEH4X{DNy<$vY@ zu9k&UiibH@qyn|kC9Eo?9-NKNqyNx0 zeyBJ~JY<+>Y;4+R7A*J7)2#h1WtkUVQ@Sq2NYzO;3$$E zIqnbs@9JW`Go1kkqGjx)4Z$HOi=7V{OvB$`2%P`mU>wgk68;ruOUq0v87ASRd5LAD zWv_X@DcIyRjxqFL{w%?R@KuKKV(}V}>|E=ZS2(a>azTFnhk`!E?VPPWf$|ddf$pT$ zz%+E6W0)aS0d0eC!3S1JkVs8X10UdSW<6;_p;%yOWNKu-WU^TJR_j~8N;*!O;3)dA!6+0Z^uGgM2{y=62e9@hHSCGW!aGrK$ z7Qzv*lHLH-(Gye?_vM!H)5R)=1kq)bOo@`w6la<%bzm_eo3W8GSv)Ir5#BLWQyPnK zoF+j!R^Ixyc{Nv(qlRO2v7@+#^N`c$8s#nRZ=fttXQ-RBG_sJ)p}~OZ7#IzC^aAa{ zg$(l^Cu|qegi}(iVX~>0w8FUFRLXe3*xAs}aE9w66mw4G!Tn$h$U#IWl85?bHBuhx zTh1_a>)a0K3ujl?8CRhDn|p(2uQ$@a)}JIlSKsI_^b>S0?FsWxYkU*8<<1MGg%Gik z&{X`*s4?p{RIDWaDYWMsb5poHe4d@l-9a*{MQgy0%-^X77V1mLZtbR4POGB?`Dgo{ z`_euCx%#;3G7L+syRW;7_l2*DY*V~S4ZSi$VUDIB88*+v9pP@F84MHuoL|WA;2ZKO zLVd0cmxSMO>+lYI4xPnK&}8%ttwz5XQe+nMK#qV5a1FB&!pLCKO!KHY{^tI@zNPMs zo+wWbPqMqb=c9Lt|GIobrs`fjoeqOv;ZQDASSkbvH-#zUC}Dz7ffxCn4CQ|tZ^T|) z7S-eu;a-M6(&=F21moZf(4XO@uHxG8H`o^hGYf|!OVo~94P~obM-KNd^H=ki^e6bM z$u4=H`lt4s+08XT8Z3)B?hsxmgfs7VF@Hh0&G2)V8HS`k*B#BpH-R5oPyzMf39vH! z#f2d|*MQ%|eClr8MO+2#Mm|=3RTl2oa>z|)Pfyo0^&+!WFR6k0KUyC$i+&+(Xc*iI zpTSosp4qD-_)S>kOBewSe;7+e!1x2dkO1=>)b6q z3m@l3B2n#LWj1MR$T7)QP&Y`UNja z<4)nJn6OA!DlW^dLjR(4Xu=sR_TC;va;{5()zEwp37fzx z3<0+seuD2AR?GxfvAVW1G?@I-+v&r#-ueZtk>*ziY1>py{ie28Tk2ob)!G5gtz95z zH6L;7dBnm{;0a_2^KItPb#y+f!yAm+!64iVZ$h8=&Ui6*1m|G|m&ZwP8afL)uycJ9 zvD0Px7d>A6$gJP3+GMsK_4QMvD!oZk^on2sjRiB9Z<5Cl#f`ysvV>Jpy`;mLEz}sc zVp!WqFdoIBG3Ycbg{Hwzw3t5B{}<5})MTZ%^3)$EC;NNIm1H0{R~{(Ol*i1AI-~W| zM(S_0b_{nuSlg>VRqtw5^{ottyoo#}8)yo{FOJiB@U^`7%K@`ieE zcy@Wad+&I^`L_F~%dh0Ua)OeqY*)T1h3azkJVU0&X-Qg1@UOr4U5J5}V9>SFXc6-YcB35l9rlAYU>5UC^I=o=YZ}vph;!hBcp5&8ZzB(@ zyUe41WU zSJBIBUA0hFneai0R}ZUxrL8htZpu*OgZ&|_eW?6};T4<6b<}j`JM7ilFdumy!&|PW z0472Y^B-%ny1AwdpC+;jzWsC-i`H(^dughgtIDhjB8njy%j(P7NN)WtO=j5mwcrSJ zvRKk6G?Se;U72>wWl>K9vcl2eGHuR~s@ItBe_!uT{Q3-ilfIq&ucprpAj_8rFx_2^ zuPB5Gomk9cr0`bA7GCgfE=YJIT;%>@D^!b1M(1HSP!B$W*{m+I3>?bp`6{FJXf}S$ zR<$zMgtzl7ip0BETq#cM!=!AoA<8gNIK%G}s*7`Y87;&6xdFH{i{h+eXGA#agG_uY zp+C2st0Qa_lKD`65;sKHDLiHVRa@aQUy3iuPL`{}N4_^cg41vm+QI5RXM>}#2(+M6 z;U^ZQD8u5KPgsVa2X_x&;%f6>xiefcx1a09RbpAYi8viWhK~J!?8+*FN3fd5AlL!@hW-ZgVOPAGRiaQ<_nQcu zq^~}f%-4IWf;y047O!Ycv_^UzN{O8~Nf7)O^kOxZ>i`EcsGr2KO4&8 ziJ!cqz3DPG`-b!~SV9wEbvy?gV-e94@Bo+!CxY|jlwO+2?M!k}d#Qe;d&m(jm6QY* z=?OBK6@Aa4*I_Aok{pMKwAFjiKBN}y#Ol2dF)V%?<^vC*{lH#^kFTN+Wf<+@49&TX z)fo4u1L-W-8x_$gR!95{4kcyu2-pLxVW-S8P)sMng>;$Pl}X}ScIrn1D^1dFf%^1~ z)(Lu;{=CELhd!%z`kMDK+J5KyfGVUL&L)ap88f-oSJHfjzHX{7hl|NTh9NJn7qDvC z`Q*4B&aAu=dM+edCh_4;dUa9(xZy2g)=J^C>~kC!N;fJBt3H~l6_OTc0*OWdWN80F z6*yS^Kn#5Y=IUd>TKG-XP!j6RqA~-8(PW;S$B%;^`4GH@tHUYEX0pOCnOQUU3^m9R zIb68Jzw*=uQb4-%q}thhUZ3w<#(PnquM0}x#wmx%1-zCND>a2*YBOad7XtQag)Ew0 z7uwJu(3h+P>(Es2oMe!y=p~=7TvZN;%|LU%4A$~Xy^}~|33xQcVlI$>czT*w)2^=C z#_OQaGZ^*Y`uaa=qlBsQVRjDo&^LiQRK`#A0D6Z+5eM=sy?uqKztGBaOrB_+P`k10Td+9xI7AR3Qbv{=zjh*ayY>XogamZWZeKM{5rZ>-=fjOWKjHMM#?^S28xB zyX9YKI39|g6L*nnz(>I zb!mLlSM zI~Zxa<`dxzezv|*X=Lh4PWpaI_4payo7^~ainAxUVHzWs*XnZVEdIAcRQ-?T0Bd`4 z$$bc|5Fh$2zG)_`yz}(7G>}_+_8Vh>+mXd>MXfz|G|6c7K62+u@91-P4|+$)^>@&k zqJQc6ZuXbIP=xF%?dzPoG4I*g!^j@s-$0NnB2rbpygnv=}&?ZVWb?`bErRjH5H zFY9B4d7d)bd&5%yEh2EazPZ|H;j1@6zG;f^Z1T<#55i5Zzvyv%n#HI_;7tEwr98J@ zwfp+>H|Q%z6{!juUtHJtoR(te%OYHk9Md1eDcUjOpa;0do=p9&xry(yta6LcAlF;t zUTwT*o$-otPI=C}p&7neQcv|a=h2|){z2aH(i&wJxk?+0R_`oWPMD(Cm-~tqF2mWE zpKS0rN^>iX)H#l17!Ja1g{}Y#zd6$qG?80UJVID4UR7UsuSolOzq3875NR&QBc1Cl zub>^oYM`^Cfgs+X_mj79>);R4T<%QmR;%xVe8f-zR?xSH#yZRn@Okn>B0zK0# zTwm#@+iLI%sY)l#h0F3LG>7~!%)+a{3d1L~7c~I4$PM(^M7+a z5U}k7BVjG;G_(m;Gdj2-@FiYg&zHBt1A*1`^Pr|xMjOavdqo(klnvBT6}g37g1fa@ zruv*g>0y}3RfSRN9&;-iFTXSmApfZYMH4!s#Gro00PlDH7}wVM)HEJT&QL+c)BS#? zW!mDAV5zp6YXRG9jj>y=O$YNHpM<7>?b;_)OYK3f!#&zv^%b{LZpBXFfyxThP=|C1 zvmM`ot6-|ORlCnDf?R#ByaLx^mY+$Uz$zt{5i{N8FEVu3j4bk6Qn;w`-bCXghE{Ga zl+hQ<=jjNpn*5Pm;D-7#&Zsd|b zUAzjDy;YHktE8_2JJlxK1>*OP6*Rg<4&Y*FYjB1k5Sywk^uyNa&J3lDd858tUP~1r zP)Sv9aWlYd&ozRL7kw%IEt2A$rTGm#edW}j{C@P*`8O`dM=P=~N~nxX{*L-J5K79^ z91SW5z*cUIx0+s>+vZzO^11t-4)hqm!>!W#e9Ql9=`5h5y1p*{?z8bpCYgAU;`dg`CaZTu_A z7;&fIy?+z7cknX5HZ6>b%I>wm*N)lKpq3r-Gwz>Y3+=xdl_Bn$aop9(GO5#2rQ z0Q6ESfROv_QvVVBCe|85;LDkHcVhw zXeP0f@R9t;io$zj2Fi7*e0MF0)9I#jC&+ky5&4Ak|BcKL+FKjTJy3RPJvhHIRXxqj z#7?T7vP(^bGenx?k#CbsI>}$4)MFaSR%M0OR#~m;$?Sj(%6hYU%AYCECJ*FYP|H)Pjv;tWGOH`W#4&s zKi4>SEmsTIPMnYL^u+iKo{7G({*FGs&*J~r|4(2Aa)Mjc;Y82AX2OKgx`q5U_6>KL zZ;O-dII@_wP}xAf_it~!dzf>qGu`2Gw04_3W8FV_Ci@!uf;bPq;cpV?CFd(+lmu-$ zPF2S6?{uk#WMfBTD|39va%%mBRu?yBUA4Zh)K zTWT4i_}|4&qL&`=ck%4-naQ90BF?FAAIgQ-v%RoXGtV}*H(oSnS}KO#Hf3>sb!M<- zN$mTn*=x&NlqHnsy4}8&%FoIncZ;IiMTeYz_e;=)PZ-u;hH{-OBT4B3FGi>c`mCa#dgd`WOPVew8zI+nS&6j!y_$R0b zmA0-|xnJfEDbYE51+%n?Y`~lr`IoI!yvlg6i&s@u9x0z-*NCUZOkShk(9`T_gJL{m z$hO?H)rt6EKN6B;YD!*uw-laEANsMI+m8HXWvQia8S+^@9JLEmi;`TA)z^Bv=}*HQ zb57_oa~yw6$qv+1qX?%xRoc^vOg8eT6X|LiC2*Eawg9d=H|lci-1vtP<;GLQ7U)r$ zkd>0R)@K5F(pvGji#@yDwaOxk^Gc?BW7r$k*|yCg?TzC_t6?DbnD$hya!>URQ%!Tr z->Tc`Eb<7P&_1rRCCPp-_FUZOk*4Sh6?()Uh&gBOFSjbYmwz(%Xz4<~-WT+jc^W$` zu63nMansVip0Aj;hKSJ3p;gSRu7@}k(Qcs@E_V(5E!B`3OY7tmWg*+taL?Eu6ikYB zL)et)Z81u8t@s*o2~pKTCTZu&U*sgcf0B%WUP|G@(ry@cmz{}0w`dP*938Od>SdKcJ(NiT52wqj^k;?pO z-4nxj{R?=G3=W$Smr?Ou{ExBiq6#7$5$CNZnPE=z$Mn~ovbvQ0>y4Lwa7CBhavgMu z-rk;W&Lxr}1T8y4uj`v4o84ZxCT0=Uf7ey+)&d@7F7~T!)UoVTeS7m|^G!>kU5o#- z^0SJ0(QP8WjC>f;BD9Y1)*n{(FsH?bUitr)?eYBMO(`8-y18t1S&p;B(bQW^hw410 zNK-%kBesLGMN8*uvTw1fZF2VUN6Oz)RV~+g>!yX&4?7q#+4>-?YHUvYsMx>k8$V=AybNq=weDUdwtLYu{j+YrdpUR{2Zxtq%9d+&S&UHyXLh9&;n}_HQC}#RXSf-27 z{V4Tx4))}FeK>nP>T9G_7BtH@wgk%yLl4tT+u4}=@#iB>hB`wwgkO(b6=LU`dx{Dk zXFg6}npdYNqilP5>!Nj~w~!e;>5B8rlTWcmv6k?OlqfmuZvFB96w*>_UrgYvUzBe` zZC;o3(kEFyw+^tpv(^YNjC4fY4ZjmMGJIyl=!kV;s$mZu=wx(4D&F*0(X0qV49McbEj|ZN*7yA48pZos|OjCasc9>q9rkZA2pV}-@QTD2~K~X(p zmc+$Jt%mO6o%n^yIcjE&ec35hBH$LEAFn{ph zSlXLHSuR-SN%wv7SN5*)+?Fcw|62B1yBq&D4GJlX2#K8*^<~6&k#nQdqx`ldroZ&{ z_#450@@78&`PJ%cwtlk$98 z^0<6U@l^L|j?wCP1{W=JXk<8imm2e`(G;3vTF1LZBL4{h3^T;G?mka&J)>fpZ}cRzUX{$iet2^mfu9KOTV}Z z+`r0k*gfWw&1_$DEz4#;N$#!+OkHicH?-XC9v)burfFH+UgK!%L35}%Ka56&#fC>Y z!+CqXh|KT?q1CPL4bz!UKDZYLp9jYJ->3tOw`@G`-5fmyq)PWwZE@ja6hq|D_N7dU-YWEj89P${R{kkgBITgmrc1Y zj1))dzSOO@e2Q3V-)*ZL**xZb_{osgVKbs`Mb@;;*ZR7mbEm#s`Yt>BY^GGu)0Gn# z6Flpg;W;J^rLCk?@>Vq^8jg?#%3Xg;Plh|w-P#|mC9%`EKlE>`MWLP$ zy}e`P$cPJJW5eU3xTr-@t?gY6@Bqqb{>qo}F{}QYNja6=ZGwIMgFRV+&(v`e>$w?d z&ejE2WitF`{>r@2Tv6yot+L>0;8^R-@@PeH&(h5>#Pn@)u{gds&H#Mxcw6ZxY zZFHOXA?ga}%d#xj3r}%ihRSl2%?XxG=3RDO^ai^S{vMh9mQgh$^XxNXn%RG18P~+@ zSsA})2cN%rd8W9&`&xM~w^#0~4hh!q-IN-WR7NBPylCY@7n>yWc=Ir}vsdS;<9_29 z;#x1SW3?B(pkOoQb)lk+?BDqT94x`@mx3R5dxOpU>BbWG7J#U@So?6mGZ8m>Y7i*bk z|16S;=n>O5HW=9lSN_s|(|+0-DjKD2`DuC9yo#^Bd;2=K&L=TH$szhD2L{UwJd!+Y zGxm~Dq#vcrGt4sYF|{&{*H6+$1b+8UaF6zQe3jtBvWUN6SQ`3Q^zSieqZ8s<#JVHd z@RMN!!v}<(F!ACxrKF_ZC+X9h^f|BQd|aQe&p%Om$*0P_nLDI6e1%5CPp~Wev5JLi zh&5)K&gx^y7I~(BrE9eF8*iMHPHPKQtPdk9#SD#4uk%y!_Vg*U?;Lkok=$HKkzY#fjb$NlTOTG zCEK~V?4Hjl)slm3E%TF5VkfcP;#b7k&m!#h3CbzMhp){ONd3eC}dL zX`qK}P#dd5=@s=CwKg|gpD7;U2k5I9zZ090Mp}I>H#p04*qag96fj61m`=JpsNpG=e2ZYM zX#L*W+kD1wQ=h1Bs5k4(Tr5qJF9(x-A3PS%GiPquf#N|$wM(v-zAgLBG1X~sSNFe^ zoN`_45B1Dab{anpr0!;YKmAbsalN9OAtnlMxgMN@pDUac#);!~$G}de>pnL$G8LQF zTlRBk>=*YYx$FNPi8;MdGcvxL3Z)oGR=A1?@;zIb9t*SLEqhQ(vWO;t}hcO zf)|VC&x1b2ON#r}|Zt0ttyp~NN&24{&?G8T|o@Ohup0^~JPKnRR6h#-< z;3+A;T->mzQbDWS899qTISS8~L^0#A>W1h$iEa3=gfl`WH;7ba z&2XL>#CI}0vWy9t5Yo;1ICQsds3p%*+ZJniqf67**NtU8+8TO4xUTd=L0;LY{FT|2 zir<#8CHm47=M&#(xvw-F=OdNbebmhLG_24kAe++!S)XQFo_t7^RU-FNzk;*FpCEQ0 z8EaU^o9&kDu$A_0)@Pw9ku|~wqu1?(PHLUHP$~%C@fEx8`M&g9N|N(tx@(n>&B`v# z_rEBgP~IUp0zNh`kkci?Xf{qhP1o}c2&1MkdZ872LYc-4=KrP>3Wp;=y4FRFA{*w8m@x6YCX{NcE;k6q3@vIEM4(0_h0Z!-bT)E z%cK22xYJAf1&fszzH3S!VFX&|wRjt=@mcO2^CkNuTL~G}8pNso3D=4k)fG4@&7&3J zxl^Fc;jG;6TqWHQLn}jX<9oBs`nhoq^3kh}ow+i~v-O!z{TAgrxTK8LYN`vA zWz1kUfr*yh1m-cBaD)Sta@DP3pJCFNEoGqN@o{HNH;kp}qcVi#(A+W^T4bAx!`6Bs>p8+<( zMiNO^@JI<<5&MRE{0}@K@*96i@jXuyM)qBHA0@B(D@rnTViYKHwh~}CvK42m;o>@7 zqyKnS9ZvtGaJ4DU&N2c`v_0e&y-eonPQ&}Gnm*f$T?wO58Fx=;L4ewHv(+{u2c zrIL-belS!XAvg9X1TvI1IPo3Bo>z39W}f&3vsF4T zJyRcQHuV)eKl6k`>=aQjWpXz8mb_hw^geXWl1FHEwLO!FS*cL0XLx12EWYDz!RNE4 zc!i&&3+n3%?_hVC$lX^1xSJMfWgt(Q3bkAkc8Vq99lFainT!@{lp0<245!t-j^Ei` z#%bzM85w;hIxyb%P^zo0lXl~j`fFVh@;7~qqs3>zMG~)8^MB@Hf@b(D{Yo2iRrLde zZR{1Hkr>IIVF!zhxK=0wPz3(UXnCbCBU3Z8Y{@;9T`l#M|<^N;>Zu_s&CaKhA3J1ezg z_DB^x1Ej~aSpKMfWKZ!n5Yqv?-hFN{w@GZlZ{%v|vUMkgM6lt?=N()MIXvTjX2hjk8&q>kVdqjM#^t3$|qg zH;0^1e^NQ^lx#(Pzas@qEqX*YgIc(Y&*R!&uvF7HlkT>7g6{w( z?hAf5ywa@5@~;!7!^7tXt{Xgq&oDlvn)-#dPF(?)ygx`+W-Y7ZPQm?eqIxfwqmET} zcxT9SN#EdBe`|7`j`1fdsoKE+6CA_5g_ln}H%WNS=kWhxBN3R&AUSR9 z6D~;z+&H- zUZyRl&($&6+=eC2OOX99_Kk~b{hOCKQ!d7SI zX{F34xUOo<&v5g*Lawk+Su4FG_g9BW4}JNdDbxI217nq{a@!#8gZfBnh1JHSjfG=V z6}B8q{z3QyegzfAQ(A>~SA%LF4Y;Lf&-aF1ib7qR`A z^HA;n250vy=k%E&8thF#Xm!MlVg5%AP{B-QoT^c4gf$m@ce<(age zdPZ5K^@8@UBib(+XPwQ-F!mGMmpM%PXzOSy7=vwg({*^ z90Bw5*Ze%F4=zCM@kUDpPkUZntKwbJh2SbKGQX1cTuc5rND?=Diuqi7KyRo=;a$ke zC4sEK$>2R)Q%5NpnvS{3BGn9ftP`B3_kmei4|=&OIj1@3RAk2Mf+f(=dvIQJYOTrF zh;Y8bdVDv2ybvoaMo-=iN4+%m1X!ajB!;vgSKw6m0Q!rm+#aqJ?oxZPpIFUV zphIa%kHfWkzI-n5xBryS?VatP4lmFzg3(fev`opRjp3)2LDJYqESMRt2!4k%wd&er zT8CPwj#j2JO#ln<5BUY2YNNpHJNe^6C**-<3g?iO>dl|wjTdY-v5BMkOChA~>?1DP9rf>@cS#v=Ef!TrMxH;@^ zcH<5VJ^S3Asc;5KX7-cLEM=FW?VZpuB=DX1<#1RG z0Zn;;E8=cq#MFhO*hlsUP*4opmQ*9{;V34MnxqS9i0?PxH$_Zs@JHY7UR|iHl{ZN(q>aH7fu(`bftmp+5GNg!M#~w>9yJ=-C>Hz4bY?Dk z-*mPQ{2Dj$3Bqc4nTHEE_*Hxx>|0D=M$@<`ej!vCI*!28?{my7duaja$%=U$^i8gh@d8tIU4LVDz$;DT+^CAm5WSqbEG`!th7eb%cHP@RKz{mMHBHp@|YLoIQu0=XCuBbpM<%i3N%pr_?CP)zn&Y+ z)#74U2WYD<;Qy+@(S8;BwWQT!&S~Kw8)q|JQG?+Xa|6BQ7UR@ffw~%xdFeUW#i1%y zZbLs7ugsDg$;HwwX@%57swG*G|2PgU);YPglBY}uJFRFArZss@p0hdF=f4Mm)>Y_< z*4Yk<@hv};-_I%ROV-X^V_jr5NhU9tZOjw20MA^}2I2L$G3!wGVKS;Ul%SoRS`<|7 zH|aJSPybMNsngW?>JW7xvW~wf)0AZR=q*e!4Uc{g6vX#1i~K;U zlZ&{TuBhzr4E*kD)O^^hbp|oF1>EWg^^`JP$-zBwDAnOK-ADaFodt?;vznkzQ6E6r zwhwJ`24rA49K`#Qow$}VvYhP)vNKh9jM=NY*ifu3ied$!BUa_BY$sNP7VIjjZ(P!f zX#!fmHD+*;9->bA1^O$%Q&8hi;Q4#tfAu3aX&trebQZmczs$i(P_BHZyh0m{Q~yxi zY9I9UM|3OhT@Edz6EJtpL4AuG%sVm@Yh@jdz&m*)-&DAViUE;AB;Sp@!Pa75k!Ili z99lBC*S=scD{2jCB0NGTY3-N?%oX&V7{ZbUOm(Oi|9h2hQ0Jg8h_-HYtJ+mvr^-sC zaz=R!r^9+wq64*+)UUQy>!Q_;t1+|}+GZ)LNHhWImWWqqPx^pWfBye#%kSJ_aK;}n zs;jdqyj2}w24k2}^t5WDCsnWdOta&aD{C1blut30v5pFerCO3CauW`qbLe)pw%SjP zh7)P3rlZ+vKe`dG-Vu7~P3lHGew_9*X1s-P$hP84=mg!2`_l>x-c05Sna4JU;^H}2 z&I|vkum}f5a6^{Cj26p%V7D>Du{tax%d{uBulF^Pc?(TLU6Mx5W87Q{54i0U+L1m4dnvI_CZ6s)H$nakk8Yh%rsNalj0+YEnNo-}7<#0nR1sxp-R1$|18 zIC0&}NK0}_3({|yk!Y1>sBMu5l42t`^VfLq@90PPGgro$qE~B;M;_L~urBJMrL>XJ znoG@9C#cKNR}SNTZ3eCVJ681XnMoKy&B-T*cPiGyl^8!7`JGK5 zi*eQK=owJ_4z(AE+D1%g=!u_WmaIr^psV-d%AYVSi2%1c0Fii0c=rB_N*6B?eJuf} zaRqfR-qUDp6G({dS{YHpi79+I$>`4 zmuAp^)H5Kl9wOqqKyTBDsP>}anl5P%F;jFyOc;x+?xY=~{SX(?E=`_*{ULak-t(<1f>ON343C;1$scRg6ZUUo0d2NLM(LMq`xsBTG<&31h{Q5%owa-|lkt;XK%8?6Mr%7AJ- zMzRrc$vKcOhsa@!q7*QLq_t2R ztIQF`48P{jQAeg&Yl^xsYvF91%*I6|Jxa zfAttwI|kkd-=lKTB~4(az~iA7XrxWd3Z@ob*9*6Y-lPNBgIT5qob;~2y><}lEPbQZ zzRx3Mby~&ukz0z)V*2E ztb*dj0~Y!cet#DqrP>v3DijBMwKdv7Z2_WSO$7pBe@Q&`v}=6A&}6#nqm{2aC!Nm>aP(tcV&`i%@@QE;JBvxZ^wE zil6$Q+KAbJH7gad$W`n^K9b9%keD#fPscw0I=o9{*2Xo)c}pM8&KbA_?7HW06R`_V z;f`@Xb5*zy{Im?7=4+wUw;`^*hwo}I=2M7fsr^z=!Ktla9g?7 zTwgqH2KJ@DZAy#aTxxJDg2s55vtN1=pO&u4M;cZ7d@>gs?YBFO25XO*BSuHlVRL9K>fm8_n_ zsfrI8nmO1Rq|#?tA#>?xh+NNL48dd+F;D{L;;=f?lFvujVHc*$Z$=IfnaRpX&k-hzZ', '', NULL, NULL, NULL, NULL, NULL), +('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, ''), +('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL), +('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL); + +-- TelecomXchange gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0), +('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1); + +-- twilio gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0), +('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0), +('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0), +('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0), +('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0), +('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0), +('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0), +('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0), +('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '.pstn.twilio.com', 32, 5060, 0, 1); + +-- voxbone gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0), +('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0), +('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0), +('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0), +('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0), +('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0), +('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1); + +-- simwood gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0), +('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0), +('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0), +('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0), +('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0), +('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0), +('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0), +('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1); diff --git a/db/create-default-account.sql b/db/create-default-account.sql index 7762f4f..66c3a79 100644 --- a/db/create-default-account.sql +++ b/db/create-default-account.sql @@ -1,4 +1,4 @@ insert into service_providers (service_provider_sid, name) values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider'); -insert into accounts (account_sid, service_provider_sid, name) -values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account'); +insert into accounts (account_sid, service_provider_sid, name, webhook_secret) +values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'foobar'); diff --git a/db/jambones-sql.sql b/db/jambones-sql.sql index 3f7a2f7..bab9340 100644 --- a/db/jambones-sql.sql +++ b/db/jambones-sql.sql @@ -2,20 +2,46 @@ SET FOREIGN_KEY_CHECKS=0; +DROP TABLE IF EXISTS account_static_ips; + +DROP TABLE IF EXISTS account_products; + +DROP TABLE IF EXISTS account_subscriptions; + +DROP TABLE IF EXISTS beta_invite_codes; + DROP TABLE IF EXISTS call_routes; +DROP TABLE IF EXISTS dns_records; + DROP TABLE IF EXISTS lcr_carrier_set_entry; DROP TABLE IF EXISTS lcr_routes; -DROP TABLE IF EXISTS api_keys; +DROP TABLE IF EXISTS predefined_sip_gateways; -DROP TABLE IF EXISTS ms_teams_tenants; +DROP TABLE IF EXISTS predefined_carriers; + +DROP TABLE IF EXISTS account_offers; + +DROP TABLE IF EXISTS products; + +DROP TABLE IF EXISTS api_keys; DROP TABLE IF EXISTS sbc_addresses; +DROP TABLE IF EXISTS ms_teams_tenants; + +DROP TABLE IF EXISTS signup_history; + +DROP TABLE IF EXISTS smpp_addresses; + +DROP TABLE IF EXISTS speech_credentials; + DROP TABLE IF EXISTS users; +DROP TABLE IF EXISTS smpp_gateways; + DROP TABLE IF EXISTS phone_numbers; DROP TABLE IF EXISTS sip_gateways; @@ -30,6 +56,41 @@ DROP TABLE IF EXISTS service_providers; DROP TABLE IF EXISTS webhooks; +CREATE TABLE account_static_ips +( +account_static_ip_sid CHAR(36) NOT NULL UNIQUE , +account_sid CHAR(36) NOT NULL, +public_ipv4 VARCHAR(16) NOT NULL UNIQUE , +private_ipv4 VARBINARY(16) NOT NULL UNIQUE , +PRIMARY KEY (account_static_ip_sid) +); + +CREATE TABLE account_subscriptions +( +account_subscription_sid CHAR(36) NOT NULL UNIQUE , +account_sid CHAR(36) NOT NULL, +pending BOOLEAN NOT NULL DEFAULT false, +effective_start_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +effective_end_date DATETIME, +change_reason VARCHAR(255), +stripe_subscription_id VARCHAR(56), +stripe_payment_method_id VARCHAR(56), +stripe_statement_descriptor VARCHAR(255), +last4 VARCHAR(512), +exp_month INTEGER, +exp_year INTEGER, +card_type VARCHAR(16), +pending_reason VARBINARY(52), +PRIMARY KEY (account_subscription_sid) +); + +CREATE TABLE beta_invite_codes +( +invite_code CHAR(6) NOT NULL UNIQUE , +in_use BOOLEAN NOT NULL DEFAULT false, +PRIMARY KEY (invite_code) +); + CREATE TABLE call_routes ( call_route_sid CHAR(36) NOT NULL UNIQUE , @@ -40,6 +101,15 @@ application_sid CHAR(36) NOT NULL, PRIMARY KEY (call_route_sid) ) COMMENT='a regex-based pattern match for call routing'; +CREATE TABLE dns_records +( +dns_record_sid CHAR(36) NOT NULL UNIQUE , +account_sid CHAR(36) NOT NULL, +record_type VARCHAR(6) NOT NULL, +record_id INTEGER NOT NULL, +PRIMARY KEY (dns_record_sid) +); + CREATE TABLE lcr_routes ( lcr_route_sid CHAR(36), @@ -49,6 +119,61 @@ priority INTEGER NOT NULL UNIQUE COMMENT 'lower priority routes are attempted f PRIMARY KEY (lcr_route_sid) ) COMMENT='Least cost routing table'; +CREATE TABLE predefined_carriers +( +predefined_carrier_sid CHAR(36) NOT NULL UNIQUE , +name VARCHAR(64) NOT NULL, +requires_static_ip BOOLEAN NOT NULL DEFAULT false, +e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers', +requires_register BOOLEAN NOT NULL DEFAULT false, +register_username VARCHAR(64), +register_sip_realm VARCHAR(64), +register_password VARCHAR(64), +tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier', +inbound_auth_username VARCHAR(64), +inbound_auth_password VARCHAR(64), +diversion VARCHAR(32), +PRIMARY KEY (predefined_carrier_sid) +); + +CREATE TABLE predefined_sip_gateways +( +predefined_sip_gateway_sid CHAR(36) NOT NULL UNIQUE , +ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.', +port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port', +inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway', +outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN', +netmask INTEGER NOT NULL DEFAULT 32, +predefined_carrier_sid CHAR(36) NOT NULL, +PRIMARY KEY (predefined_sip_gateway_sid) +); + +CREATE TABLE products +( +product_sid CHAR(36) NOT NULL UNIQUE , +name VARCHAR(32) NOT NULL, +category ENUM('api_rate','voice_call_session', 'device') NOT NULL, +PRIMARY KEY (product_sid) +); + +CREATE TABLE account_products +( +account_product_sid CHAR(36) NOT NULL UNIQUE , +account_subscription_sid CHAR(36) NOT NULL, +product_sid CHAR(36) NOT NULL, +quantity INTEGER NOT NULL, +PRIMARY KEY (account_product_sid) +); + +CREATE TABLE account_offers +( +account_offer_sid CHAR(36) NOT NULL UNIQUE , +account_sid CHAR(36) NOT NULL, +product_sid CHAR(36) NOT NULL, +stripe_product_id VARCHAR(56) NOT NULL, +PRIMARY KEY (account_offer_sid) +); + CREATE TABLE api_keys ( api_key_sid CHAR(36) NOT NULL UNIQUE , @@ -61,6 +186,15 @@ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (api_key_sid) ) COMMENT='An authorization token that is used to access the REST api'; +CREATE TABLE sbc_addresses +( +sbc_address_sid CHAR(36) NOT NULL UNIQUE , +ipv4 VARCHAR(255) NOT NULL, +port INTEGER NOT NULL DEFAULT 5060, +service_provider_sid CHAR(36), +PRIMARY KEY (sbc_address_sid) +); + CREATE TABLE ms_teams_tenants ( ms_teams_tenant_sid CHAR(36) NOT NULL UNIQUE , @@ -71,67 +205,121 @@ tenant_fqdn VARCHAR(255) NOT NULL UNIQUE , PRIMARY KEY (ms_teams_tenant_sid) ) COMMENT='A Microsoft Teams customer tenant'; -CREATE TABLE sbc_addresses +CREATE TABLE signup_history ( -sbc_address_sid CHAR(36) NOT NULL UNIQUE , +email VARCHAR(255) NOT NULL, +name VARCHAR(255), +signed_up_at DATETIME DEFAULT CURRENT_TIMESTAMP, +PRIMARY KEY (email) +); + +CREATE TABLE smpp_addresses +( +smpp_address_sid CHAR(36) NOT NULL UNIQUE , ipv4 VARCHAR(255) NOT NULL, port INTEGER NOT NULL DEFAULT 5060, +use_tls BOOLEAN NOT NULL DEFAULT 0, +is_primary BOOLEAN NOT NULL DEFAULT 1, service_provider_sid CHAR(36), -PRIMARY KEY (sbc_address_sid) +PRIMARY KEY (smpp_address_sid) +); + +CREATE TABLE speech_credentials +( +speech_credential_sid CHAR(36) NOT NULL UNIQUE , +service_provider_sid CHAR(36), +account_sid CHAR(36), +vendor VARCHAR(32) NOT NULL, +credential VARCHAR(8192) NOT NULL, +use_for_tts BOOLEAN DEFAULT true, +use_for_stt BOOLEAN DEFAULT true, +last_used DATETIME, +last_tested DATETIME, +tts_tested_ok BOOLEAN, +stt_tested_ok BOOLEAN, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +PRIMARY KEY (speech_credential_sid) ); CREATE TABLE users ( user_sid CHAR(36) NOT NULL UNIQUE , -name CHAR(36) NOT NULL UNIQUE , -hashed_password VARCHAR(1024) NOT NULL, -salt CHAR(16) NOT NULL, -force_change BOOLEAN NOT NULL DEFAULT TRUE, +name VARCHAR(255) NOT NULL, +email VARCHAR(255) NOT NULL, +pending_email VARCHAR(255), +phone VARCHAR(20) UNIQUE , +hashed_password VARCHAR(1024), +account_sid CHAR(36), +service_provider_sid CHAR(36), +force_change BOOLEAN NOT NULL DEFAULT FALSE, +provider VARCHAR(255) NOT NULL, +provider_userid VARCHAR(255), +scope VARCHAR(16) NOT NULL DEFAULT 'read-write', +phone_activation_code VARCHAR(16), +email_activation_code VARCHAR(16), +email_validated BOOLEAN NOT NULL DEFAULT false, +phone_validated BOOLEAN NOT NULL DEFAULT false, +email_content_opt_out BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (user_sid) ); CREATE TABLE voip_carriers ( voip_carrier_sid CHAR(36) NOT NULL UNIQUE , -name VARCHAR(64) NOT NULL UNIQUE , +name VARCHAR(64) NOT NULL, description VARCHAR(255), -account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a customer PBX that is associated with a specific account', +account_sid CHAR(36) COMMENT 'if provided, indicates this entity represents a sip trunk that is associated with a specific account', +service_provider_sid CHAR(36), application_sid CHAR(36) COMMENT 'If provided, all incoming calls from this source will be routed to the associated application', -e164_leading_plus BOOLEAN NOT NULL DEFAULT false, +e164_leading_plus BOOLEAN NOT NULL DEFAULT false COMMENT 'if true, a leading plus should be prepended to outbound phone numbers', requires_register BOOLEAN NOT NULL DEFAULT false, register_username VARCHAR(64), register_sip_realm VARCHAR(64), register_password VARCHAR(64), -tech_prefix VARCHAR(16), +tech_prefix VARCHAR(16) COMMENT 'tech prefix to prepend to outbound calls to this carrier', +inbound_auth_username VARCHAR(64), +inbound_auth_password VARCHAR(64), diversion VARCHAR(32), is_active BOOLEAN NOT NULL DEFAULT true, +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +smpp_system_id VARCHAR(255), +smpp_password VARCHAR(64), +smpp_enquire_link_interval INTEGER DEFAULT 0, +smpp_inbound_system_id VARCHAR(255), +smpp_inbound_password VARCHAR(64), PRIMARY KEY (voip_carrier_sid) ) COMMENT='A Carrier or customer PBX that can send or receive calls'; +CREATE TABLE smpp_gateways +( +smpp_gateway_sid CHAR(36) NOT NULL UNIQUE , +ipv4 VARCHAR(128) NOT NULL, +port INTEGER NOT NULL DEFAULT 2775, +netmask INTEGER NOT NULL DEFAULT 32, +is_primary BOOLEAN NOT NULL DEFAULT 1, +inbound BOOLEAN NOT NULL DEFAULT 0 COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway', +outbound BOOLEAN NOT NULL DEFAULT 1 COMMENT 'if true, include in least-cost routing when placing calls to the PSTN', +use_tls BOOLEAN DEFAULT 0, +voip_carrier_sid CHAR(36) NOT NULL, +PRIMARY KEY (smpp_gateway_sid) +); + CREATE TABLE phone_numbers ( phone_number_sid CHAR(36) UNIQUE , number VARCHAR(32) NOT NULL UNIQUE , -voip_carrier_sid CHAR(36) NOT NULL, +voip_carrier_sid CHAR(36), account_sid CHAR(36), application_sid CHAR(36), +service_provider_sid CHAR(36) COMMENT 'if not null, this number is a test number for the associated service provider', PRIMARY KEY (phone_number_sid) -) ENGINE=InnoDB COMMENT='A phone number that has been assigned to an account'; - -CREATE TABLE webhooks -( -webhook_sid CHAR(36) NOT NULL UNIQUE , -url VARCHAR(1024) NOT NULL, -method ENUM("GET","POST") NOT NULL DEFAULT 'POST', -username VARCHAR(255), -password VARCHAR(255), -PRIMARY KEY (webhook_sid) -) COMMENT='An HTTP callback'; +) COMMENT='A phone number that has been assigned to an account'; CREATE TABLE sip_gateways ( sip_gateway_sid CHAR(36), ipv4 VARCHAR(128) NOT NULL COMMENT 'ip address or DNS name of the gateway. For gateways providing inbound calling service, ip address is required.', +netmask INTEGER NOT NULL DEFAULT 32, port INTEGER NOT NULL DEFAULT 5060 COMMENT 'sip signaling port', inbound BOOLEAN NOT NULL COMMENT 'if true, whitelist this IP to allow inbound calls from the gateway', outbound BOOLEAN NOT NULL COMMENT 'if true, include in least-cost routing when placing calls to the PSTN', @@ -150,11 +338,22 @@ priority INTEGER NOT NULL DEFAULT 0 COMMENT 'lower priority carriers are attempt PRIMARY KEY (lcr_carrier_set_entry_sid) ) COMMENT='An entry in the LCR routing list'; +CREATE TABLE webhooks +( +webhook_sid CHAR(36) NOT NULL UNIQUE , +url VARCHAR(1024) NOT NULL, +method ENUM("GET","POST") NOT NULL DEFAULT 'POST', +username VARCHAR(255), +password VARCHAR(255), +PRIMARY KEY (webhook_sid) +) COMMENT='An HTTP callback'; + CREATE TABLE applications ( application_sid CHAR(36) NOT NULL UNIQUE , name VARCHAR(64) NOT NULL, -account_sid CHAR(36) NOT NULL COMMENT 'account that this application belongs to', +service_provider_sid CHAR(36) COMMENT 'if non-null, this application is a test application that can be used by any account under the associated service provider', +account_sid CHAR(36) COMMENT 'account that this application belongs to (if null, this is a service provider test application)', call_hook_sid CHAR(36) COMMENT 'webhook to call for inbound calls ', call_status_hook_sid CHAR(36) COMMENT 'webhook to call for call status events', messaging_hook_sid CHAR(36) COMMENT 'webhook to call for inbound SMS/MMS ', @@ -163,6 +362,7 @@ speech_synthesis_language VARCHAR(12) NOT NULL DEFAULT 'en-US', speech_synthesis_voice VARCHAR(64), speech_recognizer_vendor VARCHAR(64) NOT NULL DEFAULT 'google', speech_recognizer_language VARCHAR(64) NOT NULL DEFAULT 'en-US', +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (application_sid) ) COMMENT='A defined set of behaviors to be applied to phone calls '; @@ -184,69 +384,144 @@ name VARCHAR(64) NOT NULL, sip_realm VARCHAR(132) UNIQUE COMMENT 'sip domain that will be used for devices registering under this account', service_provider_sid CHAR(36) NOT NULL COMMENT 'service provider that owns the customer relationship with this account', registration_hook_sid CHAR(36) COMMENT 'webhook to call when devices underr this account attempt to register', +queue_event_hook_sid CHAR(36), device_calling_application_sid CHAR(36) COMMENT 'application to use for outbound calling from an account', is_active BOOLEAN NOT NULL DEFAULT true, -webhook_secret VARCHAR(36), +created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +plan_type ENUM('trial','free','paid') NOT NULL DEFAULT 'trial', +stripe_customer_id VARCHAR(56), +webhook_secret VARCHAR(36) NOT NULL, disable_cdrs BOOLEAN NOT NULL DEFAULT 0, +trial_end_date DATETIME, +deactivated_reason VARCHAR(255), +device_to_call_ratio INTEGER NOT NULL DEFAULT 5, PRIMARY KEY (account_sid) ) COMMENT='An enterprise that uses the platform for comm services'; +CREATE INDEX account_static_ip_sid_idx ON account_static_ips (account_static_ip_sid); +CREATE INDEX account_sid_idx ON account_static_ips (account_sid); +ALTER TABLE account_static_ips ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX account_subscription_sid_idx ON account_subscriptions (account_subscription_sid); +CREATE INDEX account_sid_idx ON account_subscriptions (account_sid); +ALTER TABLE account_subscriptions ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX invite_code_idx ON beta_invite_codes (invite_code); CREATE INDEX call_route_sid_idx ON call_routes (call_route_sid); -ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk (account_sid) REFERENCES accounts (account_sid); +ALTER TABLE call_routes ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE call_routes ADD FOREIGN KEY application_sid_idxfk (application_sid) REFERENCES applications (application_sid); +CREATE INDEX dns_record_sid_idx ON dns_records (dns_record_sid); +ALTER TABLE dns_records ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX predefined_carrier_sid_idx ON predefined_carriers (predefined_carrier_sid); +CREATE INDEX predefined_sip_gateway_sid_idx ON predefined_sip_gateways (predefined_sip_gateway_sid); +CREATE INDEX predefined_carrier_sid_idx ON predefined_sip_gateways (predefined_carrier_sid); +ALTER TABLE predefined_sip_gateways ADD FOREIGN KEY predefined_carrier_sid_idxfk (predefined_carrier_sid) REFERENCES predefined_carriers (predefined_carrier_sid); + +CREATE INDEX product_sid_idx ON products (product_sid); +CREATE INDEX account_product_sid_idx ON account_products (account_product_sid); +CREATE INDEX account_subscription_sid_idx ON account_products (account_subscription_sid); +ALTER TABLE account_products ADD FOREIGN KEY account_subscription_sid_idxfk (account_subscription_sid) REFERENCES account_subscriptions (account_subscription_sid); + +ALTER TABLE account_products ADD FOREIGN KEY product_sid_idxfk (product_sid) REFERENCES products (product_sid); + +CREATE INDEX account_offer_sid_idx ON account_offers (account_offer_sid); +CREATE INDEX account_sid_idx ON account_offers (account_sid); +ALTER TABLE account_offers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX product_sid_idx ON account_offers (product_sid); +ALTER TABLE account_offers ADD FOREIGN KEY product_sid_idxfk_1 (product_sid) REFERENCES products (product_sid); + CREATE INDEX api_key_sid_idx ON api_keys (api_key_sid); CREATE INDEX account_sid_idx ON api_keys (account_sid); -ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_1 (account_sid) REFERENCES accounts (account_sid); +ALTER TABLE api_keys ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid); CREATE INDEX service_provider_sid_idx ON api_keys (service_provider_sid); ALTER TABLE api_keys ADD FOREIGN KEY service_provider_sid_idxfk (service_provider_sid) REFERENCES service_providers (service_provider_sid); -CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid); -ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid); - -ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_2 (account_sid) REFERENCES accounts (account_sid); - -ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid); - -CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn); CREATE INDEX sbc_addresses_idx_host_port ON sbc_addresses (ipv4,port); CREATE INDEX sbc_address_sid_idx ON sbc_addresses (sbc_address_sid); CREATE INDEX service_provider_sid_idx ON sbc_addresses (service_provider_sid); -ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid); +ALTER TABLE sbc_addresses ADD FOREIGN KEY service_provider_sid_idxfk_1 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + +CREATE INDEX ms_teams_tenant_sid_idx ON ms_teams_tenants (ms_teams_tenant_sid); +ALTER TABLE ms_teams_tenants ADD FOREIGN KEY service_provider_sid_idxfk_2 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + +ALTER TABLE ms_teams_tenants ADD FOREIGN KEY account_sid_idxfk_6 (account_sid) REFERENCES accounts (account_sid); + +ALTER TABLE ms_teams_tenants ADD FOREIGN KEY application_sid_idxfk_1 (application_sid) REFERENCES applications (application_sid); + +CREATE INDEX tenant_fqdn_idx ON ms_teams_tenants (tenant_fqdn); +CREATE INDEX email_idx ON signup_history (email); +CREATE INDEX smpp_address_sid_idx ON smpp_addresses (smpp_address_sid); +CREATE INDEX service_provider_sid_idx ON smpp_addresses (service_provider_sid); +ALTER TABLE smpp_addresses ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + +CREATE UNIQUE INDEX speech_credentials_idx_1 ON speech_credentials (vendor,account_sid); + +CREATE INDEX speech_credential_sid_idx ON speech_credentials (speech_credential_sid); +CREATE INDEX service_provider_sid_idx ON speech_credentials (service_provider_sid); +ALTER TABLE speech_credentials ADD FOREIGN KEY service_provider_sid_idxfk_4 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + +CREATE INDEX account_sid_idx ON speech_credentials (account_sid); +ALTER TABLE speech_credentials ADD FOREIGN KEY account_sid_idxfk_7 (account_sid) REFERENCES accounts (account_sid); CREATE INDEX user_sid_idx ON users (user_sid); -CREATE INDEX name_idx ON users (name); +CREATE INDEX email_idx ON users (email); +CREATE INDEX phone_idx ON users (phone); +CREATE INDEX account_sid_idx ON users (account_sid); +ALTER TABLE users ADD FOREIGN KEY account_sid_idxfk_8 (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX service_provider_sid_idx ON users (service_provider_sid); +ALTER TABLE users ADD FOREIGN KEY service_provider_sid_idxfk_5 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + +CREATE INDEX email_activation_code_idx ON users (email_activation_code); CREATE INDEX voip_carrier_sid_idx ON voip_carriers (voip_carrier_sid); -CREATE INDEX name_idx ON voip_carriers (name); -ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_3 (account_sid) REFERENCES accounts (account_sid); +CREATE INDEX account_sid_idx ON voip_carriers (account_sid); +ALTER TABLE voip_carriers ADD FOREIGN KEY account_sid_idxfk_9 (account_sid) REFERENCES accounts (account_sid); + +CREATE INDEX service_provider_sid_idx ON voip_carriers (service_provider_sid); +ALTER TABLE voip_carriers ADD FOREIGN KEY service_provider_sid_idxfk_6 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE voip_carriers ADD FOREIGN KEY application_sid_idxfk_2 (application_sid) REFERENCES applications (application_sid); -CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid); -CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid); -ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); +CREATE INDEX smpp_gateway_sid_idx ON smpp_gateways (smpp_gateway_sid); +CREATE INDEX voip_carrier_sid_idx ON smpp_gateways (voip_carrier_sid); +ALTER TABLE smpp_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); -ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_4 (account_sid) REFERENCES accounts (account_sid); +CREATE INDEX phone_number_sid_idx ON phone_numbers (phone_number_sid); +CREATE INDEX number_idx ON phone_numbers (number); +CREATE INDEX voip_carrier_sid_idx ON phone_numbers (voip_carrier_sid); +ALTER TABLE phone_numbers ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); + +ALTER TABLE phone_numbers ADD FOREIGN KEY account_sid_idxfk_10 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE phone_numbers ADD FOREIGN KEY application_sid_idxfk_3 (application_sid) REFERENCES applications (application_sid); -CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid); -CREATE UNIQUE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port); +CREATE INDEX service_provider_sid_idx ON phone_numbers (service_provider_sid); +ALTER TABLE phone_numbers ADD FOREIGN KEY service_provider_sid_idxfk_7 (service_provider_sid) REFERENCES service_providers (service_provider_sid); -ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_1 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); +CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port); + +CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid); +ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY lcr_route_sid_idxfk (lcr_route_sid) REFERENCES lcr_routes (lcr_route_sid); -ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); +ALTER TABLE lcr_carrier_set_entry ADD FOREIGN KEY voip_carrier_sid_idxfk_3 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid); +CREATE INDEX webhook_sid_idx ON webhooks (webhook_sid); CREATE UNIQUE INDEX applications_idx_name ON applications (account_sid,name); CREATE INDEX application_sid_idx ON applications (application_sid); +CREATE INDEX service_provider_sid_idx ON applications (service_provider_sid); +ALTER TABLE applications ADD FOREIGN KEY service_provider_sid_idxfk_8 (service_provider_sid) REFERENCES service_providers (service_provider_sid); + CREATE INDEX account_sid_idx ON applications (account_sid); -ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_5 (account_sid) REFERENCES accounts (account_sid); +ALTER TABLE applications ADD FOREIGN KEY account_sid_idxfk_11 (account_sid) REFERENCES accounts (account_sid); ALTER TABLE applications ADD FOREIGN KEY call_hook_sid_idxfk (call_hook_sid) REFERENCES webhooks (webhook_sid); @@ -262,10 +537,12 @@ ALTER TABLE service_providers ADD FOREIGN KEY registration_hook_sid_idxfk (regis CREATE INDEX account_sid_idx ON accounts (account_sid); CREATE INDEX sip_realm_idx ON accounts (sip_realm); CREATE INDEX service_provider_sid_idx ON accounts (service_provider_sid); -ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_3 (service_provider_sid) REFERENCES service_providers (service_provider_sid); +ALTER TABLE accounts ADD FOREIGN KEY service_provider_sid_idxfk_9 (service_provider_sid) REFERENCES service_providers (service_provider_sid); ALTER TABLE accounts ADD FOREIGN KEY registration_hook_sid_idxfk_1 (registration_hook_sid) REFERENCES webhooks (webhook_sid); +ALTER TABLE accounts ADD FOREIGN KEY queue_event_hook_sid_idxfk (queue_event_hook_sid) REFERENCES webhooks (webhook_sid); + ALTER TABLE accounts ADD FOREIGN KEY device_calling_application_sid_idxfk (device_calling_application_sid) REFERENCES applications (application_sid); SET FOREIGN_KEY_CHECKS=1; diff --git a/db/jambones.sqs b/db/jambones.sqs index 97f83a1..c7466ba 100644 --- a/db/jambones.sqs +++ b/db/jambones.sqs @@ -1,15 +1,298 @@ + + + + + 411.00 + 390.00 + + + 283.00 + 220.00 + + 25 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + voip_carrier_sid + voip_carriers + + + 4 + 1 + + + + + + + + + + + + + + + 1791.00 + 608.00 + + + 294.00 + 100.00 + + 15 + + + + 1 + + + + + + + + + account_subscription_sid + account_subscriptions + + + 3 + 1 + + + + + + + + + + product_sid + products + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + 467.00 + 1115.00 + + + 336.00 + 160.00 + + 21 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + predefined_carrier_sid + predefined_carriers + + + 4 + 1 + + + + + + + + + + + + + + + 1334.00 + 1169.00 + + + 298.00 + 100.00 + + 18 + + + + 1 + + + + + + + + + account_sid + accounts + + + 3 + 1 + + + + + + + + + + + + + + + + + + + + + + + + - 1599.00 - 37.00 + 1745.00 + 19.00 - 250.00 - 120.00 + 316.00 + 360.00 11 @@ -23,46 +306,207 @@ - - + + + + + + + + + + + + + + + + + + + + + + - + - - - - + + + account_sid + accounts + + + 4 + 2 + + + + + + + + + service_provider_sid + service_providers + + + 4 + 2 + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1140.00 + 12.00 + + + 282.00 + 140.00 + + 26 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + service_provider_sid + service_providers + + + 4 + 1 + + + + + + + + + - 417.00 - 263.00 + 24.00 + 317.00 - 266.00 - 280.00 + 267.00 + 460.00 6 @@ -80,10 +524,10 @@ - + - + @@ -101,9 +545,24 @@ 2 - + + + + + + service_provider_sid + service_providers + + + 4 + 2 + + + + + @@ -123,6 +582,7 @@ + @@ -150,21 +610,67 @@ - + + + + + + + + + + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -174,8 +680,8 @@ - 1319.00 - 38.00 + 1459.00 + 15.00 245.00 @@ -246,21 +752,209 @@ - + + + + + + 1354.00 + 821.00 + + + 368.00 + 280.00 + + 14 + + + + 1 + + + + + + + + + service_provider_sid + service_providers + + + 4 + 2 + + + + + + + + + account_sid + accounts + + + 4 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1386.00 + 333.00 + + + 276.00 + 100.00 + + 22 + + + + 1 + + + + + + + + + account_sid + accounts + + + 4 + 1 + + + + + + + + + + product_sid + products + + + 4 + 1 + + + + + + + + + + + + + + + + - 1315.00 - 376.00 + 483.00 + 887.00 - 254.00 + 249.00 120.00 10 @@ -297,7 +991,7 @@ - + @@ -307,8 +1001,8 @@ - 407.00 - 584.00 + 36.00 + 791.00 254.00 @@ -367,18 +1061,86 @@ - + + + + + + 2123.00 + 24.00 + + + 215.00 + 80.00 + + 24 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + 1796.00 + 780.00 + + + 232.00 + 60.00 + + 23 + + + + 1 + + + + + + + + + + + + + + + + - 1309.00 - 219.00 + 425.00 + 633.00 298.00 @@ -445,7 +1207,7 @@ - + @@ -455,8 +1217,8 @@ - 63.00 - 315.00 + 22.00 + 155.00 259.00 @@ -503,6 +1265,7 @@ 1 + @@ -514,7 +1277,7 @@ - + @@ -524,12 +1287,12 @@ - 825.00 - 321.00 + 827.00 + 327.00 - 380.00 - 200.00 + 296.00 + 340.00 4 @@ -584,12 +1347,25 @@ 4 - 2 + 1 + + + + webhook_sid + webhooks + + + 4 + 1 + + + + @@ -611,36 +1387,252 @@ + + + + + + + + + + + + + + + + + + + + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + 1779.00 + 427.00 + + + 352.00 + 80.00 + + 16 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + 47.00 + 1103.00 + + + 326.00 + 260.00 + + 20 + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 960.00 + 1155.00 + + + 262.00 + 100.00 + + 17 + + + + 1 + + + + + + + + + account_sid + accounts + + + 4 + 1 + + + + + + + + + + + + + + + + + + + + + - - 360.00 - 753.00 + 33.00 + 924.00 331.00 - 120.00 + 140.00 2 @@ -657,6 +1649,7 @@ + @@ -673,7 +1666,7 @@ - + @@ -706,7 +1699,22 @@ - + + + + service_provider_sid + service_providers + + + 4 + 2 + + + + + + + @@ -720,8 +1728,8 @@ 17.00 - 310.00 - 180.00 + 260.00 + 200.00 7 @@ -737,6 +1745,13 @@ + + + + + + + @@ -748,6 +1763,7 @@ + @@ -770,6 +1786,7 @@ 1 + @@ -795,10 +1812,9 @@ - - + @@ -808,12 +1824,12 @@ - 829.00 - 568.00 + 837.00 + 741.00 345.00 - 260.00 + 300.00 0 @@ -833,6 +1849,21 @@ + + + + service_provider_sid + service_providers + + + 2 + 2 + + + + + + @@ -841,12 +1872,12 @@ 4 - 1 + 2 - - + + @@ -926,6 +1957,14 @@ + + + + + + + + @@ -944,17 +1983,124 @@ - + + + + + + 1367.00 + 477.00 + + + 322.00 + 300.00 + + 19 + + + + 1 + + + + + + + + + + account_sid + accounts + + + 3 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - 1305.00 - 564.00 + 772.00 + 13.00 281.00 @@ -965,6 +2111,7 @@ 1 + @@ -1014,7 +2161,7 @@ - + @@ -1023,8 +2170,8 @@ - 66.00 - 176.00 + 24.00 + 32.00 233.00 @@ -1059,7 +2206,7 @@ - + @@ -1069,8 +2216,8 @@ - 838.00 - 96.00 + 837.00 + 160.00 293.00 @@ -1126,14 +2273,14 @@ - + - + @@ -1145,17 +2292,17 @@ - + - - - - + + + + diff --git a/db/reset_admin_password.js b/db/reset_admin_password.js index 147c1e7..2d5083f 100755 --- a/db/reset_admin_password.js +++ b/db/reset_admin_password.js @@ -1,71 +1,46 @@ #!/usr/bin/env node -const {getMysqlConnection} = require('../lib/db'); -const crypto = require('crypto'); +console.log('reset_admin_password'); +const {promisePool} = require('../lib/db'); const uuidv4 = require('uuid/v4'); +const {generateHashedPassword} = require('../lib/utils/password-utils'); const sqlInsert = `INSERT into users -(user_sid, name, hashed_password, salt) -values (?, ?, ?, ?) +(user_sid, name, email, hashed_password, force_change, provider, email_validated) +values (?, ?, ?, ?, ?, ?, ?) `; const sqlChangeAdminToken = `UPDATE api_keys set token = ? WHERE account_sid IS NULL AND service_provider_sid IS NULL`; +const sqlQueryAccount = 'SELECT * from accounts LIMIT 1'; +const sqlAddAccountAdminToken = `INSERT into api_keys (api_key_sid, token, account_sid) +VALUES (?, ?, ?)`; -/** - * generates random string of characters i.e salt - * @function - * @param {number} length - Length of the random string. - */ -const genRandomString = (len) => { - return crypto.randomBytes(Math.ceil(len / 2)) - .toString('hex') /** convert to hexadecimal format */ - .slice(0, len); /** return required number of characters */ -}; - -/** - * hash password with sha512. - * @function - * @param {string} password - List of required fields. - * @param {string} salt - Data to be validated. - */ -const sha512 = function(password, salt) { - const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */ - hash.update(password); - var value = hash.digest('hex'); - return { - salt:salt, - passwordHash:value - }; -}; - -const saltHashPassword = (userpassword) => { - var salt = genRandomString(16); /** Gives us salt of length 16 */ - return sha512(userpassword, salt); -}; - -/* reset admin password */ -getMysqlConnection((err, conn) => { - if (err) return console.log(err, 'Error connecting to database'); - - /* delete admin user if it exists */ - conn.query('DELETE from users where name = "admin"', (err) => { - if (err) return console.log(err, 'Error removing admin user'); - const {salt, passwordHash} = saltHashPassword('admin'); - const sid = uuidv4(); - conn.query(sqlInsert, [ +const doIt = async() => { + const passwordHash = await generateHashedPassword('admin'); + const sid = uuidv4(); + await promisePool.execute('DELETE from users where name = "admin"'); + await promisePool.execute(sqlInsert, + [ sid, 'admin', + 'joe@foo.bar', passwordHash, - salt - ], (err) => { - if (err) return console.log(err, 'Error inserting admin user'); - console.log('successfully reset admin password'); - const uuid = uuidv4(); - conn.query(sqlChangeAdminToken, [uuid], (err) => { - if (err) return console.log(err, 'Error updating admin token'); - console.log('successfully changed admin tokens'); - conn.release(); - process.exit(0); - }); - }); - }); -}); + 1, + 'local', + 1 + ] + ); + + /* reset admin token */ + const uuid = uuidv4(); + await promisePool.query(sqlChangeAdminToken, [uuid]); + + /* create admin token for single account */ + const api_key_sid = uuidv4(); + const token = uuidv4(); + const [r] = await promisePool.query(sqlQueryAccount); + await promisePool.execute(sqlAddAccountAdminToken, [api_key_sid, token, r[0].account_sid]); + + process.exit(0); +}; + +doIt(); diff --git a/db/seed-integration-test.sql b/db/seed-integration-test.sql new file mode 100644 index 0000000..42f35ac --- /dev/null +++ b/db/seed-integration-test.sql @@ -0,0 +1,102 @@ +SET FOREIGN_KEY_CHECKS=0; + +insert into sbc_addresses (sbc_address_sid, ipv4, port) +values('f6567ae1-bf97-49af-8931-ca014b689995', '52.55.111.178', 5060); +insert into sbc_addresses (sbc_address_sid, ipv4, port) +values('de5ed2f1-bccd-4600-a95e-cef46e9a3a4f', '3.34.102.122', 5060); +insert into smpp_addresses (smpp_address_sid, ipv4, port, use_tls, is_primary) +values('de5ed2f1-bccd-4600-a95e-cef46e9a3a4f', '34.197.99.29', 2775, 0, 1); +insert into smpp_addresses (smpp_address_sid, ipv4, port, use_tls, is_primary) +values('049078a0', '3.209.58.102', 3550, 1, 1); + +-- create one service provider and account +insert into api_keys (api_key_sid, token) +values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de'); + +-- create one service provider and one account +insert into service_providers (service_provider_sid, name, root_domain) +values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider', 'sip.jambonz.us'); + +insert into accounts (account_sid, service_provider_sid, name, webhook_secret) +values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk'); + +-- create two applications +insert into webhooks(webhook_sid, url, method) +values +('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'), +('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'), +('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST'); + +insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language) +VALUES +('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'), +('4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'dial time', '81844b05-714d-4295-8bf3-3b0640a4bf02', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'); + +-- create our products +insert into products (product_sid, name, category) +values +('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'), +('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'), +('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate'); + +-- create predefined carriers +insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus, +requires_register, register_username, register_password, +register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion) +VALUES +('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL), +('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, ''), +('17479288-bb9f-421a-89d1-f4ac57af1dca', 'Peerless Network (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('bdf70650-5328-47aa-b3d0-47cb219d9c6e', '382 Communications (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL); + +-- twilio gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0), +('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0), +('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0), +('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0), +('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0), +('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0), +('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0), +('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0), +('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '.pstn.twilio.com', 32, 5060, 0, 1); + +-- voxbone gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0), +('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0), +('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0), +('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0), +('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0), +('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0), +('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1); + +-- Peerless gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0), +('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1), +('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1); + +-- 382com gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0), +('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1); + +-- simwood gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0), +('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0), +('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0), +('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0), +('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0), +('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0), +('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0), +('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/db/seed-production-database-open-source.sql b/db/seed-production-database-open-source.sql new file mode 100644 index 0000000..38c32cf --- /dev/null +++ b/db/seed-production-database-open-source.sql @@ -0,0 +1,101 @@ +SET FOREIGN_KEY_CHECKS=0; + +-- create one service provider and account +insert into api_keys (api_key_sid, token) +values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de'); + +-- create one service provider and one account +insert into service_providers (service_provider_sid, name) +values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'default service provider'); + +insert into accounts (account_sid, service_provider_sid, name, webhook_secret) +values ('9351f46a-678c-43f5-b8a6-d4eb58d131af','2708b1b3-2736-40ea-b502-c53d8396247f', 'default account', 'wh_secret_cJqgtMDPzDhhnjmaJH6Mtk'); + +insert into api_keys (api_key_sid, token, account_sid) +values ('09e92f3c-9d73-4303-b63f-3668574862ce', '1cf2f4f4-64c4-4249-9a3e-5bb4cb597c2a', '9351f46a-678c-43f5-b8a6-d4eb58d131af'); + +-- create two applications +insert into webhooks(webhook_sid, url, method) +values +('84e3db00-b172-4e46-b54b-a503fdb19e4a', 'https://public-apps.jambonz.us/call-status', 'POST'), +('d31568d0-b193-4a05-8ff6-778369bc6efe', 'https://public-apps.jambonz.us/hello-world', 'POST'), +('81844b05-714d-4295-8bf3-3b0640a4bf02', 'https://public-apps.jambonz.us/dial-time', 'POST'); + +insert into applications (application_sid, account_sid, name, call_hook_sid, call_status_hook_sid, speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language) +VALUES +('7087fe50-8acb-4f3b-b820-97b573723aab', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'hello world', 'd31568d0-b193-4a05-8ff6-778369bc6efe', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'), +('4ca2fb6a-8636-4f2e-96ff-8966c5e26f8e', '9351f46a-678c-43f5-b8a6-d4eb58d131af', 'dial time', '81844b05-714d-4295-8bf3-3b0640a4bf02', '84e3db00-b172-4e46-b54b-a503fdb19e4a', 'google', 'en-US', 'en-US-Wavenet-C', 'google', 'en-US'); + +-- create our products +insert into products (product_sid, name, category) +values +('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'), +('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'), +('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate'); + +-- create predefined carriers +insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus, +requires_register, register_username, register_password, +register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion) +VALUES +('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL), +('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL), +('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, ''), +('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL); + +-- TelecomXchange gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0), +('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1); + +-- twilio gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0), +('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0), +('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0), +('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0), +('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0), +('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0), +('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0), +('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0), +('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '.pstn.twilio.com', 32, 5060, 0, 1); + +-- voxbone gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0), +('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0), +('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0), +('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0), +('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0), +('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0), +('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1); + +-- Peerless gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0), +('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1), +('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1); + +-- 382com gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0), +('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1); + +-- simwood gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0), +('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0), +('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0), +('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0), +('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0), +('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0), +('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0), +('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1); + +SET FOREIGN_KEY_CHECKS=1; diff --git a/db/seed-production-database.sql b/db/seed-production-database.sql new file mode 100644 index 0000000..8cf8f44 --- /dev/null +++ b/db/seed-production-database.sql @@ -0,0 +1,78 @@ +-- create one service provider +insert into service_providers (service_provider_sid, name, description, root_domain) +values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'sip.jambonz.us', 'jambonz.us service provider', 'sip.jambonz.us'); +insert into api_keys (api_key_sid, token) +values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de'); + +-- create our products +insert into products (product_sid, name, category) +values +('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'), +('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'), +('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate'); + +-- create predefined carriers +insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus, +requires_register, register_username, register_password, +register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion) +VALUES +('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL), +('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, ''), +('17479288-bb9f-421a-89d1-f4ac57af1dca', 'TelecomsXChange', 0, 0, 0, NULL, NULL, NULL, 'your-tech-prefix', NULL, NULL, NULL), +('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL); + +-- TelecomXchange gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('c9c3643e-9a83-4b78-b172-9c09d911bef5', '17479288-bb9f-421a-89d1-f4ac57af1dca', '174.136.44.213', 32, 5060, 1, 0), +('3b5b7fa5-4e61-4423-b921-05c3283b2101', '17479288-bb9f-421a-89d1-f4ac57af1dca', 'sip01.TelecomsXChange.com', 32, 5060, 0, 1); + +-- twilio gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0), +('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0), +('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0), +('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0), +('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0), +('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0), +('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0), +('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0), +('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '.pstn.twilio.com', 32, 5060, 0, 1); + +-- voxbone gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0), +('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0), +('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0), +('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0), +('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0), +('7bae60b3-4237-4baa-a711-30ea3bce19d8', '032d90d5-39e8-41c0-b807-9c88cffba65c', '185.47.148.45', 32, 5060, 1, 0), +('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1); + +-- Peerless gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0), +('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1), +('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1); + +-- 382com gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0), +('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1); + +-- simwood gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.77', 32, 5060, 1, 0), +('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.77', 32, 5060, 1, 0), +('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.77', 32, 5060, 1, 0), +('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.77', 32, 5060, 1, 0), +('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.77', 32, 5060, 1, 0), +('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.34', 32, 5060, 1, 0), +('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.66', 32, 5060, 1, 0), +('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1); + diff --git a/db/webapp-tests.sql b/db/webapp-tests.sql new file mode 100644 index 0000000..e43716d --- /dev/null +++ b/db/webapp-tests.sql @@ -0,0 +1,105 @@ +SET FOREIGN_KEY_CHECKS=0; + +-- create one service provider +insert into service_providers (service_provider_sid, name, description, root_domain) +values ('2708b1b3-2736-40ea-b502-c53d8396247f', 'jambonz.us', 'jambonz.us service provider', 'sip.yakeeda.com'); +insert into api_keys (api_key_sid, token) +values ('3f35518f-5a0d-4c2e-90a5-2407bb3b36f0', '38700987-c7a4-4685-a5bb-af378f9734de'); + +-- one sbc +insert into sbc_addresses (sbc_address_sid, service_provider_sid, ipv4, port) values ('8d6d0fda-4550-41ab-8e2f-60761d81fe7d', null, '3.39.45.30', '5060'); + +-- two smpp server +insert into smpp_addresses (smpp_address_sid, service_provider_sid, ipv4, port, use_tls, is_primary) values ('e5e8345b-d533-4c29-940b-57aaccc59f8b', null, '3.39.45.30', '2775', false, true); +insert into smpp_addresses (smpp_address_sid, service_provider_sid, ipv4, port, use_tls, is_primary) values ('ae060ef3-d5a4-4842-b331-426ec9329fbe', null, '3.39.45.30', '3550', true, true); + +-- one voip carrier with one gateway +insert into voip_carriers (voip_carrier_sid, name) values ('5145b436-2f38-4029-8d4c-fd8c67831c7a', 'my test carrier'); +insert into sip_gateways (sip_gateway_sid, voip_carrier_sid, ipv4, port, inbound, outbound, is_active) +values ('46b727eb-c7dc-44fa-b063-96e48d408e4a', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '3.3.3.3', 5060, 1, 1, 1); + +-- create the test application and test phone number +insert into webhooks (webhook_sid, url, method) values ('d9c205c6-a129-443e-a9c0-d1bb437d4bb7', 'https://flows.jambonz.us/testCall', 'POST'); +insert into webhooks (webhook_sid, url, method) values ('6ac36aeb-6bd0-428a-80a1-aed95640a296', 'https://flows.jambonz.us/callStatus', 'POST'); +insert into applications (application_sid, name, service_provider_sid, call_hook_sid, call_status_hook_sid, +speech_synthesis_vendor, speech_synthesis_language, speech_synthesis_voice, speech_recognizer_vendor, speech_recognizer_language) +values ('7a489343-02ed-471e-8df0-fc5e1b98ce8f', 'Test application', '2708b1b3-2736-40ea-b502-c53d8396247f', +'d9c205c6-a129-443e-a9c0-d1bb437d4bb7','6ac36aeb-6bd0-428a-80a1-aed95640a296','google', 'en-US', 'en-US-Standard-C', 'google', 'en-US'); +insert into phone_numbers (phone_number_sid, number, voip_carrier_sid, service_provider_sid) +values ('ec028c46-1363-4b3f-81db-ee33f179d6ba', '18005551212', '5145b436-2f38-4029-8d4c-fd8c67831c7a', '2708b1b3-2736-40ea-b502-c53d8396247f'); + +-- create our products +insert into products (product_sid, name, category) +values +('c4403cdb-8e75-4b27-9726-7d8315e3216d', 'concurrent call session', 'voice_call_session'), +('2c815913-5c26-4004-b748-183b459329df', 'registered device', 'device'), +('35a9fb10-233d-4eb9-aada-78de5814d680', 'api call', 'api_rate'); + +-- create predefined carriers +insert into predefined_carriers (predefined_carrier_sid, name, requires_static_ip, e164_leading_plus, +requires_register, register_username, register_password, +register_sip_realm, tech_prefix, inbound_auth_username, inbound_auth_password, diversion) +VALUES +('7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', 'Twilio', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL), +('032d90d5-39e8-41c0-b807-9c88cffba65c', 'Voxbone', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, ''), +('17479288-bb9f-421a-89d1-f4ac57af1dca', 'Peerless Network (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('bdf70650-5328-47aa-b3d0-47cb219d9c6e', '382 Communications (US)', 1, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL), +('e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'Simwood', 0, 1, 0, '', '', NULL, NULL, NULL, NULL, NULL); + +-- twilio gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d2ccfcb1-9198-4fe9-a0ca-6e49395837c4', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.172.60.0', 30, 5060, 1, 0), +('6b1d0032-4430-41f1-87c6-f22233d394ef', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.244.51.0', 30, 5060, 1, 0), +('0de40217-8bd5-4aa8-a9fd-1994282953c6', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.171.127.192', 30, 5060, 1, 0), +('37bc0b20-b53c-4c31-95a6-f82b1c3713e3', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '35.156.191.128', 30, 5060, 1, 0), +('39791f4e-b612-4882-a37e-e92711a39f3f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.65.63.192', 30, 5060, 1, 0), +('81a0c8cb-a33e-42da-8f20-99083da6f02f', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.252.254.64', 30, 5060, 1, 0), +('eeeef07a-46b8-4ffe-a4f2-04eb32ca889e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '54.169.127.128', 30, 5060, 1, 0), +('fbb6c194-4b68-4dff-9b42-52412be1c39e', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '177.71.206.192', 30, 5060, 1, 0), +('3ed1dd12-e1a7-44ff-811a-3cc5dc13dc72', '7d509a18-bbff-4c5d-b21e-b99bf8f8c49a', '.pstn.twilio.com', 32, 5060, 0, 1); + +-- voxbone gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('d531c582-2103-42a0-b9f0-f80c215b3ec5', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.83.45', 32, 5060, 1, 0), +('95c888e5-c959-4d92-82c4-8597dddff75e', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.86.45', 32, 5060, 1, 0), +('1de3b2a1-96f0-407a-bcc4-ce371d823a8d', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.82.45', 32, 5060, 1, 0), +('50c1f91a-6080-4495-a241-6bba6e9d9688', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.85.45', 32, 5060, 1, 0), +('e6ebad33-80d5-4dbb-bc6f-a7ae08160cc6', '032d90d5-39e8-41c0-b807-9c88cffba65c', '81.201.84.45', 32, 5060, 1, 0), +('bc933522-18a2-47d8-9ae4-9faa8de4e927', '032d90d5-39e8-41c0-b807-9c88cffba65c', 'outbound.voxbone.com', 32, 5060, 0, 1); + +-- Peerless gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('4e23f698-a70a-4616-9bf0-c9dd5ab123af', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.182', 32, 5060, 1, 0), +('e5c71c18-0511-41b8-bed9-1ba061bbcf10', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.52.192', 32, 5060, 0, 1), +('226c7471-2f4f-440f-8525-37fd0512bd8b', '17479288-bb9f-421a-89d1-f4ac57af1dca', '208.79.54.185', 32, 5060, 0, 1); + +-- 382com gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('23e4c250-8578-4d88-99b5-a7941a58e26f', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.125.111.10', 32, 5060, 1, 0), +('c726d435-c9a7-4c37-b891-775990a54638', 'bdf70650-5328-47aa-b3d0-47cb219d9c6e', '64.124.67.11', 32, 5060, 0, 1); + +-- simwood gateways +insert into predefined_sip_gateways (predefined_sip_gateway_sid, predefined_carrier_sid, ipv4, netmask, port, inbound, outbound) +VALUES +('91cb050f-9826-4ac9-b736-84a10372a9fe', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '149.91.14.0', 24, 5060, 1, 0), +('58700fad-98bf-4d31-b61e-888c54911b35', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '154.51.137.96', 27, 5060, 1, 0), +('d020fd9e-7fdb-4bca-ae0d-e61b38142873', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '78.40.245.160', 27, 5060, 1, 0), +('441fd2e7-c845-459c-963d-6e917063ed9a', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.24', 29, 5060, 1, 0), +('1e0d3e80-9973-4184-9bec-07ae564f983f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.136.28', 28, 5060, 1, 0), +('e56ec745-5f37-443f-afb4-7bbda31ae7ac', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.48', 28, 5060, 1, 0), +('e7447e7e-2c7d-4738-ab53-097c187236ff', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.140.242', 32, 5060, 1, 0), +('279807e7-649e-41dd-931a-00c460bcc0a2', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.143.80', 28, 5060, 1, 0), +('f5fd66f7-97f6-4979-ab19-eea1557bc872', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.140.0', 26, 5060, 1, 0), +('efcfefdc-451c-4e59-960a-f9e4952d964f', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.141.0', 26, 5060, 1, 0), +('b6ae6240-55ac-4c11-892f-a71b2155ea60', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.142.0', 26, 5060, 1, 0), +('5a976337-164b-408e-8748-d8bfb4bd5d76', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '185.63.143.0', 26, 5060, 1, 0), +('ed0434ca-7f26-4624-9523-0419d0d2924d', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '178.22.139.0', 26, 5060, 1, 0), +('d1a594c2-c14f-4ead-b621-96129bc87886', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', '172.86.224.0', 24, 5060, 1, 0), +('5f431d42-48e4-44ce-a311-d946f0b475b6', 'e6fb301a-1af0-4fb8-a1f6-f65530c6e1c6', 'out.simwood.com', 32, 5060, 0, 1); + + +SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/lib/auth/index.js b/lib/auth/index.js index c7e7745..0ea0a43 100644 --- a/lib/auth/index.js +++ b/lib/auth/index.js @@ -1,56 +1,103 @@ const Strategy = require('passport-http-bearer').Strategy; const {getMysqlConnection} = require('../db'); +const {hashString} = require('../utils/password-utils'); +const debug = require('debug')('jambonz:api-server'); +const jwt = require('jsonwebtoken'); const sql = ` SELECT * FROM api_keys WHERE api_keys.token = ?`; -function makeStrategy(logger) { +function makeStrategy(logger, retrieveKey) { return new Strategy( - function(token, done) { - logger.info(`validating with token ${token}`); - getMysqlConnection((err, conn) => { + async function(token, done) { + logger.debug(`validating with token ${token}`); + jwt.verify(token, process.env.JWT_SECRET, async(err, decoded) => { if (err) { - logger.error(err, 'Error retrieving mysql connection'); - return done(err); - } - conn.query(sql, [token], (err, results, fields) => { - conn.release(); - if (err) { - logger.error(err, 'Error querying for api key'); - return done(err); - } - if (0 == results.length) return done(null, false); - if (results.length > 1) { - logger.info(`api key ${token} exists in multiple rows of api_keys table!!`); + if (err.name === 'TokenExpiredError') { + logger.debug('jwt expired'); return done(null, false); } - - // found api key - const scope = []; - if (results[0].account_sid === null && results[0].service_provider_sid === null) { - scope.push.apply(scope, ['admin', 'service_provider', 'account']); + /* its not a jwt obtained through login, check api leys */ + checkApiTokens(logger, token, done); + } + else { + /* validated -- make sure it is not on blacklist */ + try { + const s = `jwt:${hashString(token)}`; + const result = await retrieveKey(s); + if (result) { + debug(`result from searching for ${s}: ${result}`); + logger.info('jwt invalidated after logout'); + return done(null, false); + } + } catch (err) { + debug(err); + logger.info({err}, 'Error checking blacklist for jwt'); } - else if (results[0].service_provider_sid) { - scope.push.apply(scope, ['service_provider', 'account']); - } - else { - scope.push('account'); - } - + const {user_sid, account_sid, email, name} = decoded; + //logger.debug({user_sid, account_sid}, 'successfully validated jwt'); + const scope = ['account']; const user = { - account_sid: results[0].account_sid, - service_provider_sid: results[0].service_provider_sid, - hasScope: (s) => scope.includes(s), - hasAdminAuth: scope.length === 3, - hasServiceProviderAuth: scope.includes('service_provider') && !scope.includes('admin'), - hasAccountAuth: scope.includes('account') && !scope.includes('service_provider') + account_sid, + user_sid, + jwt: token, + email, + name, + hasScope: (s) => s === 'account', + hasAdminAuth: false, + hasServiceProviderAuth: false, + hasAccountAuth: true }; - logger.info(user, `successfully validated with scope ${scope}`); return done(null, user, {scope}); - }); + } }); - }); + } + ); } +const checkApiTokens = (logger, token, done) => { + getMysqlConnection((err, conn) => { + if (err) { + logger.error(err, 'Error retrieving mysql connection'); + return done(err); + } + conn.query(sql, [token], (err, results, fields) => { + conn.release(); + if (err) { + logger.error(err, 'Error querying for api key'); + return done(err); + } + if (0 == results.length) return done(null, false); + if (results.length > 1) { + logger.info(`api key ${token} exists in multiple rows of api_keys table!!`); + return done(null, false); + } + + // found api key + const scope = []; + if (results[0].account_sid === null && results[0].service_provider_sid === null) { + scope.push.apply(scope, ['admin', 'service_provider', 'account']); + } + else if (results[0].service_provider_sid) { + scope.push.apply(scope, ['service_provider', 'account']); + } + else { + scope.push('account'); + } + + const user = { + account_sid: results[0].account_sid, + service_provider_sid: results[0].service_provider_sid, + hasScope: (s) => scope.includes(s), + hasAdminAuth: scope.length === 3, + hasServiceProviderAuth: scope.includes('service_provider'), + hasAccountAuth: scope.includes('account') && !scope.includes('service_provider') + }; + logger.info(user, `successfully validated with scope ${scope}`); + return done(null, user, {scope}); + }); + }); +}; + module.exports = makeStrategy; diff --git a/lib/db/index.js b/lib/db/index.js index 9d77afc..93f4546 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -1,5 +1,7 @@ const getMysqlConnection = require('./mysql'); +const promisePool = require('./pool'); module.exports = { - getMysqlConnection + getMysqlConnection, + promisePool }; diff --git a/lib/db/mysql.js b/lib/db/mysql.js index 5a70a32..d1cbe3a 100644 --- a/lib/db/mysql.js +++ b/lib/db/mysql.js @@ -1,8 +1,8 @@ const mysql = require('mysql2'); const pool = mysql.createPool({ host: process.env.JAMBONES_MYSQL_HOST, - user: process.env.JAMBONES_MYSQL_USER, port: process.env.JAMBONES_MYSQL_PORT || 3306, + user: process.env.JAMBONES_MYSQL_USER, password: process.env.JAMBONES_MYSQL_PASSWORD, database: process.env.JAMBONES_MYSQL_DATABASE, connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10 @@ -13,6 +13,7 @@ pool.getConnection((err, conn) => { conn.ping((err) => { if (err) return console.error(err, `Error pinging mysql at ${JSON.stringify({ host: process.env.JAMBONES_MYSQL_HOST, + port: process.env.JAMBONES_MYSQL_PORT || 3306, user: process.env.JAMBONES_MYSQL_USER, password: process.env.JAMBONES_MYSQL_PASSWORD, database: process.env.JAMBONES_MYSQL_DATABASE, diff --git a/lib/db/pool.js b/lib/db/pool.js new file mode 100644 index 0000000..5cc3feb --- /dev/null +++ b/lib/db/pool.js @@ -0,0 +1,10 @@ +const mysql = require('mysql2'); +const pool = mysql.createPool({ + host: process.env.JAMBONES_MYSQL_HOST, + port: process.env.JAMBONES_MYSQL_PORT || 3306, + user: process.env.JAMBONES_MYSQL_USER, + password: process.env.JAMBONES_MYSQL_PASSWORD, + database: process.env.JAMBONES_MYSQL_DATABASE, + connectionLimit: process.env.JAMBONES_MYSQL_CONNECTION_LIMIT || 10 +}); +module.exports = pool.promise(); diff --git a/lib/models/account.js b/lib/models/account.js index db52e60..21b2732 100644 --- a/lib/models/account.js +++ b/lib/models/account.js @@ -1,10 +1,57 @@ +const debug = require('debug')('jambonz:api-server'); const Model = require('./model'); const {getMysqlConnection} = require('../db'); +const {promisePool} = require('../db'); +const uuid = require('uuid').v4; +const {encrypt} = require('../utils/encrypt-decrypt'); const retrieveSql = `SELECT * from accounts acc LEFT JOIN webhooks AS rh ON acc.registration_hook_sid = rh.webhook_sid`; +const insertPendingAccountSubscriptionSql = `INSERT account_subscriptions +(account_subscription_sid, account_sid, pending, stripe_subscription_id, +stripe_payment_method_id, last4, exp_month, exp_year, card_type) +VALUES (?,?,1,?,?,?,?,?,?)`; + +const activateSubscriptionSql = `UPDATE account_subscriptions +SET pending=0, effective_start_date = CURRENT_TIMESTAMP, stripe_subscription_id = ? +WHERE account_subscription_sid = ? +AND pending=1`; + +const queryPendingSubscriptionSql = `SELECT * FROM account_subscriptions +WHERE account_sid = ? +AND effective_end_date IS NULL +AND pending=1`; + +const deactivateSubscriptionSql = `UPDATE account_subscriptions +SET pending=1, pending_reason = ? +WHERE account_sid = ? +AND effective_end_date IS NULL +AND pending=0`; + +const updatePaymentInfoSql = `UPDATE account_subscriptions +SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ? +WHERE account_sid = ? +AND effective_end_date IS NULL`; + +const insertAccountProductsSql = `INSERT account_products +(account_product_sid, account_subscription_sid, product_sid, quantity) +VALUES (?,?,?,?); +`; + +const replaceOldSubscriptionSql = `UPDATE account_subscriptions +SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ? +WHERE account_sid = ? +AND effective_end_date IS NULL +AND account_subscription_sid <> ?`; + +const retrieveActiveSubscriptionSql = `SELECT * +FROM account_subscriptions +WHERE account_sid = ? +AND effective_end_date IS NULL +AND pending = 0`; + function transmogrifyResults(results) { return results.map((row) => { const obj = row.acc; @@ -73,6 +120,111 @@ class Account extends Model { }); } + static async updateStripeCustomerId(sid, customerId) { + await promisePool.execute( + 'UPDATE accounts SET stripe_customer_id = ? WHERE account_sid = ?', + [customerId, sid]); + } + + static async getSubscription(sid) { + const [r] = await promisePool.execute(retrieveActiveSubscriptionSql, [sid]); + debug(r, `Account.getSubscription ${sid}`); + return r.length > 0 ? r[0] : null; + } + + static async deactivateSubscription(logger, account_sid, reason) { + logger.debug('deactivateSubscription'); + + /** + * Two cases: + * (1) A subscription renewal fails. In this case we deactivate subscription + * and the customer is down until they provide payment. + * (2) A customer adds capacity during the month, and the pro-rated amount fails. + * In this case, we leave the new subscription in a pending state + * The customer continues (for the rest of the month at least) at + * previous capacity levels. + */ + const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid); + if (r.length > 0) { + /* leave new subscription pending */ + await promisePool.execute( + 'UPDATE account_subscriptions set pending_reason = ? WHERE account_subscription_sid = ?', + [reason, r[0].account_subscription_sid]); + logger.debug('deactivateSubscription - leave pending subscription in pending state'); + } + else { + /* deactivate their current active subscription */ + const [r] = await promisePool.execute(deactivateSubscriptionSql, [reason, account_sid]); + logger.debug('deactivateSubscription - deactivated subscription; customer will not have service'); + return 1 == r.affectedRows; + } + } + + static async activateSubscription(logger, account_sid, subscription_id, reason) { + logger.debug('activateSubscription'); + + const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid); + if (0 === r.length) return false; + + const [r2] = await promisePool.execute(activateSubscriptionSql, + [subscription_id, r[0].account_subscription_sid]); + if (0 === r2.affectedRows) return false; + + /* disable the old subscription, if any */ + const [r3] = await promisePool.execute(replaceOldSubscriptionSql, [ + reason, account_sid, r[0].account_subscription_sid]); + debug(r3, 'Account.activateSubscription - replaced old subscription'); + + /* update account.plan to paid, if it isnt already */ + await promisePool.execute( + 'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?', + [account_sid]); + return true; + } + + static async updatePaymentInfo(logger, account_sid, pm) { + const {card} = pm; + const last4_encrypted = encrypt(card.last4); + await promisePool.execute(updatePaymentInfoSql, + [last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]); + } + + static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) { + logger.debug('provisionPendingSubscription'); + const account_subscription_sid = uuid(); + const {id, card} = payment_method; + + /* add a row to account_subscription */ + let last4_encrypted = null; + if (card) { + last4_encrypted = encrypt(card.last4); + } + const [r] = await promisePool.execute(insertPendingAccountSubscriptionSql, [ + account_subscription_sid, + account_sid, + subscription_id || null, + id, + last4_encrypted, + card ? card.exp_month : null, + card ? card.exp_year : null, + card ? card.brand : null + ]); + debug(r, 'Account.activateSubscription - insert account_subscriptions'); + if (r.affectedRows !== 1) { + throw new Error(`failed inserting account_subscriptions for accunt_sid ${account_sid}`); + } + + /* add a row for each product to account_products */ + await Promise.all(products.map((product) => { + const {product_sid, quantity} = product; + const account_products_sid = uuid(); + return promisePool.execute(insertAccountProductsSql, [ + account_products_sid, account_subscription_sid, product_sid, quantity + ]); + })); + return account_subscription_sid; + } + } Account.table = 'accounts'; @@ -103,6 +255,30 @@ Account.fields = [ { name: 'device_calling_application_sid', type: 'string', + }, + { + name: 'is_active', + type: 'number', + }, + { + name: 'created_at', + type: 'date', + }, + { + name: 'plan_type', + type: 'string', + }, + { + name: 'stripe_customer_id', + type: 'string', + }, + { + name: 'webhook_secret', + type: 'string', + }, + { + name: 'disable_cdrs', + type: 'number', } ]; diff --git a/lib/models/phone-number.js b/lib/models/phone-number.js index 4e6295a..d430659 100644 --- a/lib/models/phone-number.js +++ b/lib/models/phone-number.js @@ -1,9 +1,45 @@ const Model = require('./model'); +const {getMysqlConnection} = require('../db'); +const sql = 'SELECT * from phone_numbers WHERE account_sid = ?'; class PhoneNumber extends Model { constructor() { super(); } + + static retrieveAll(account_sid) { + if (!account_sid) return super.retrieveAll(); + + return new Promise((resolve, reject) => { + getMysqlConnection((err, conn) => { + if (err) return reject(err); + conn.query(sql, account_sid, (err, results, fields) => { + conn.release(); + if (err) return reject(err); + resolve(results); + }); + }); + }); + } + + /** + * retrieve an application + */ + static retrieve(sid, account_sid) { + if (!account_sid) return super.retrieve(sid); + + return new Promise((resolve, reject) => { + getMysqlConnection((err, conn) => { + if (err) return reject(err); + conn.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid], (err, results, fields) => { + conn.release(); + if (err) return reject(err); + resolve(results); + }); + }); + }); + } + } PhoneNumber.table = 'phone_numbers'; @@ -20,8 +56,7 @@ PhoneNumber.fields = [ }, { name: 'voip_carrier_sid', - type: 'string', - required: true + type: 'string' }, { name: 'account_sid', diff --git a/lib/models/predefined-carrier.js b/lib/models/predefined-carrier.js new file mode 100644 index 0000000..abd7e01 --- /dev/null +++ b/lib/models/predefined-carrier.js @@ -0,0 +1,63 @@ +const Model = require('./model'); + +class PredefinedCarrier extends Model { + constructor() { + super(); + } +} + +PredefinedCarrier.table = 'predefined_carriers'; +PredefinedCarrier.fields = [ + { + name: 'predefined_carrier_sid', + type: 'string', + primaryKey: true + }, + { + name: 'name', + type: 'string', + required: true + }, + { + name: 'requires_static_ip', + type: 'number' + }, + { + name: 'e164_leading_plus', + type: 'number' + }, + { + name: 'requires_register', + type: 'number' + }, + { + name: 'register_username', + type: 'string' + }, + { + name: 'register_sip_realm', + type: 'string' + }, + { + name: 'register_password', + type: 'string' + }, + { + name: 'tech_prefix', + type: 'string' + }, + { + name: 'inbound_auth_username', + type: 'string' + }, + { + name: 'inbound_auth_password', + type: 'string' + }, + { + name: 'diversion', + type: 'string' + }, +]; + +module.exports = PredefinedCarrier; diff --git a/lib/models/product.js b/lib/models/product.js new file mode 100644 index 0000000..847d33e --- /dev/null +++ b/lib/models/product.js @@ -0,0 +1,28 @@ +const Model = require('./model'); + +class Product extends Model { + constructor() { + super(); + } +} + +Product.table = 'products'; +Product.fields = [ + { + name: 'product_sid', + type: 'string', + primaryKey: true + }, + { + name: 'name', + type: 'string', + required: true + }, + { + name: 'category', + type: 'string', + required: true + }, +]; + +module.exports = Product; diff --git a/lib/models/sip-gateway.js b/lib/models/sip-gateway.js index d6849bd..7a316a2 100644 --- a/lib/models/sip-gateway.js +++ b/lib/models/sip-gateway.js @@ -1,9 +1,18 @@ const Model = require('./model'); +const {promisePool} = require('../db'); +const retrieveSql = 'SELECT * from sip_gateways WHERE voip_carrier_sid = ?'; class SipGateway extends Model { constructor() { super(); } + /** + * list all sip gateways for a voip_carrier + */ + static async retrieveForVoipCarrier(voip_carrier_sid) { + const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid); + return rows; + } } SipGateway.table = 'sip_gateways'; @@ -26,6 +35,10 @@ SipGateway.fields = [ name: 'port', type: 'number' }, + { + name: 'netmask', + type: 'number' + }, { name: 'inbound', type: 'number' diff --git a/lib/models/smpp.js b/lib/models/smpp.js new file mode 100644 index 0000000..b234b19 --- /dev/null +++ b/lib/models/smpp.js @@ -0,0 +1,55 @@ +const Model = require('./model'); +const {getMysqlConnection} = require('../db'); + +class Smpp extends Model { + constructor() { + super(); + } + + /** + * list all SBCs either for a given service provider, or those not associated with a + * service provider (i.e. community SBCs) + */ + static retrieveAll(service_provider_sid) { + const sql = service_provider_sid ? + 'SELECT * from smpp_addresses WHERE service_provider_sid = ?' : + 'SELECT * from smpp_addresses WHERE service_provider_sid IS NULL'; + const args = service_provider_sid ? [service_provider_sid] : []; + + return new Promise((resolve, reject) => { + getMysqlConnection((err, conn) => { + if (err) return reject(err); + conn.query(sql, args, (err, results) => { + conn.release(); + if (err) return reject(err); + resolve(results); + }); + }); + }); + } + +} + +Smpp.table = 'smpp_addresses'; +Smpp.fields = [ + { + name: 'smpp_address_sid', + type: 'string', + primaryKey: true + }, + { + name: 'ipv4', + type: 'string', + required: true + }, + { + name: 'port', + type: 'number' + }, + { + name: 'service_provider_sid', + type: 'string' + } +]; + +module.exports = Smpp; diff --git a/lib/models/smpp_gateway.js b/lib/models/smpp_gateway.js new file mode 100644 index 0000000..f073af2 --- /dev/null +++ b/lib/models/smpp_gateway.js @@ -0,0 +1,60 @@ +const Model = require('./model'); +const {promisePool} = require('../db'); +const retrieveSql = 'SELECT * from smpp_gateways WHERE voip_carrier_sid = ?'; + +class SmppGateway extends Model { + constructor() { + super(); + } + /** + * list all sip gateways for a voip_carrier + */ + static async retrieveForVoipCarrier(voip_carrier_sid) { + const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid); + return rows; + } +} + +SmppGateway.table = 'smpp_gateways'; +SmppGateway.fields = [ + { + name: 'smpp_gateway_sid', + type: 'string', + primaryKey: true + }, + { + name: 'voip_carrier_sid', + type: 'string' + }, + { + name: 'ipv4', + type: 'string', + required: true + }, + { + name: 'port', + type: 'number' + }, + { + name: 'netmask', + type: 'number' + }, + { + name: 'inbound', + type: 'number' + }, + { + name: 'outbound', + type: 'number' + }, + { + name: 'is_primary', + type: 'number' + }, + { + name: 'use_tls', + type: 'number' + } +]; + +module.exports = SmppGateway; diff --git a/lib/models/speech-credential.js b/lib/models/speech-credential.js new file mode 100644 index 0000000..ff529cc --- /dev/null +++ b/lib/models/speech-credential.js @@ -0,0 +1,92 @@ +const Model = require('./model'); +const {promisePool} = require('../db'); +const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?'; +const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?'; + +class SpeechCredential extends Model { + constructor() { + super(); + } + + /** + * list all credentials for an account + */ + static async retrieveAll(account_sid) { + const [rows] = await promisePool.query(retrieveSql, account_sid); + return rows; + } + static async retrieveAllForSP(service_provider_sid) { + const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid); + return rows; + } + + static async disableStt(account_sid) { + await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]); + } + static async disableTts(account_sid) { + await promisePool.execute('UPDATE speech_credentials SET use_for_tts = 0 WHERE account_sid = ?', [account_sid]); + } + + static async ttsTestResult(sid, success) { + await promisePool.execute( + 'UPDATE speech_credentials SET last_tested = NOW(), tts_tested_ok = ? WHERE speech_credential_sid = ?', + [success, sid]); + } + static async sttTestResult(sid, success) { + await promisePool.execute( + 'UPDATE speech_credentials SET last_tested = NOW(), stt_tested_ok = ? WHERE speech_credential_sid = ?', + [success, sid]); + } +} + +SpeechCredential.table = 'speech_credentials'; +SpeechCredential.fields = [ + { + name: 'speech_credential_sid', + type: 'string', + primaryKey: true + }, + { + name: 'account_sid', + type: 'string', + }, + { + name: 'service_provider_sid', + type: 'string', + }, + { + name: 'vendor', + type: 'string', + required: true, + }, + { + name: 'credential', + type: 'string', + }, + { + name: 'use_for_tts', + type: 'number' + }, + { + name: 'use_for_stt', + type: 'number' + }, + { + name: 'tts_tested_ok', + type: 'number' + }, + { + name: 'stt_tested_ok', + type: 'number' + }, + { + name: 'last_used', + type: 'date' + }, + { + name: 'last_tested', + type: 'date' + } +]; + +module.exports = SpeechCredential; diff --git a/lib/models/voip-carrier.js b/lib/models/voip-carrier.js index ca17860..f744dbe 100644 --- a/lib/models/voip-carrier.js +++ b/lib/models/voip-carrier.js @@ -1,9 +1,22 @@ const Model = require('./model'); +const {promisePool} = require('../db'); +const retrieveSql = 'SELECT * from voip_carriers vc WHERE vc.account_sid = ?'; +const retrieveSqlForSP = 'SELECT * from voip_carriers vc WHERE vc.service_provider_sid = ?'; + class VoipCarrier extends Model { constructor() { super(); } + static async retrieveAll(account_sid) { + if (!account_sid) return super.retrieveAll(); + const [rows] = await promisePool.query(retrieveSql, account_sid); + return rows; + } + static async retrieveAllForSP(service_provider_sid) { + const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid); + return rows; + } } VoipCarrier.table = 'voip_carriers'; @@ -26,6 +39,10 @@ VoipCarrier.fields = [ name: 'account_sid', type: 'string', }, + { + name: 'service_provider_sid', + type: 'string', + }, { name: 'application_sid', type: 'string' @@ -49,7 +66,51 @@ VoipCarrier.fields = [ { name: 'register_password', type: 'string' - } + }, + { + name: 'tech_prefix', + type: 'string' + }, + { + name: 'inbound_auth_username', + type: 'string' + }, + { + name: 'inbound_auth_password', + type: 'string' + }, + { + name: 'diversion', + type: 'string' + }, + { + name: 'is_active', + type: 'number' + }, + { + name: 'smpp_system_id', + type: 'string' + }, + { + name: 'smpp_password', + type: 'string' + }, + { + name: 'smpp_inbound_system_id', + type: 'string' + }, + { + name: 'smpp_inbound_password', + type: 'string' + }, + { + name: 'smpp_enquire_link_interval', + type: 'number' + }, + { + name: 'smpp_system_id', + type: 'string' + }, ]; module.exports = VoipCarrier; diff --git a/lib/routes/api/account-test.js b/lib/routes/api/account-test.js new file mode 100644 index 0000000..cca0e00 --- /dev/null +++ b/lib/routes/api/account-test.js @@ -0,0 +1,57 @@ +const router = require('express').Router(); +const {promisePool} = require('../../db'); +const sysError = require('../error'); +const retrieveApplicationsSql = `SELECT * from applications app +LEFT JOIN webhooks AS ch +ON app.call_hook_sid = ch.webhook_sid +LEFT JOIN webhooks AS sh +ON app.call_status_hook_sid = sh.webhook_sid +LEFT JOIN webhooks AS mh +ON app.messaging_hook_sid = mh.webhook_sid +WHERE service_provider_sid = ?`; + +const transmogrifyResults = (results) => { + return results.map((row) => { + const obj = row.app; + if (row.ch && Object.keys(row.ch).length && row.ch.url !== null) { + Object.assign(obj, {call_hook: row.ch}); + } + else obj.call_hook = null; + if (row.sh && Object.keys(row.sh).length && row.sh.url !== null) { + Object.assign(obj, {call_status_hook: row.sh}); + } + else obj.call_status_hook = null; + if (row.mh && Object.keys(row.mh).length && row.mh.url !== null) { + Object.assign(obj, {messaging_hook: row.mh}); + } + else obj.messaging_hook = null; + delete obj.call_hook_sid; + delete obj.call_status_hook_sid; + delete obj.messaging_hook_sid; + return obj; + }); +}; + +router.get('/:service_provider_sid', async(req, res) => { + const logger = req.app.locals.logger; + const {service_provider_sid} = req.params; + + try { + const [r] = await promisePool.query('SELECT * from service_providers where service_provider_sid = ?', + service_provider_sid); + if (r.length === 0) { + logger.info(`/AccountTest invalid service_provider_sid ${service_provider_sid}`); + return res.sendStatus(404); + } + + const [numbers] = await promisePool.query('SELECT number FROM phone_numbers WHERE service_provider_sid = ?', + service_provider_sid); + const [results] = await promisePool.query({sql: retrieveApplicationsSql, nestTables: true}, service_provider_sid); + + res.json({phonenumbers: numbers.map((n) => n.number), applications: transmogrifyResults(results)}); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/accounts.js b/lib/routes/api/accounts.js index 0c0a45f..e5e2eb2 100644 --- a/lib/routes/api/accounts.js +++ b/lib/routes/api/accounts.js @@ -2,20 +2,65 @@ const router = require('express').Router(); const request = require('request'); const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const Account = require('../../models/account'); +const Application = require('../../models/application'); const Webhook = require('../../models/webhook'); const ApiKey = require('../../models/api-key'); const ServiceProvider = require('../../models/service-provider'); +const {deleteDnsRecords} = require('../../utils/dns-utils'); +const {deleteCustomer} = require('../../utils/stripe-utils'); const uuidv4 = require('uuid/v4'); -const decorate = require('./decorate'); const snakeCase = require('../../utils/snake-case'); -const sysError = require('./error'); -const preconditions = { - 'add': validateAdd, - 'update': validateUpdate, - 'delete': validateDelete -}; +const sysError = require('../error'); +const {promisePool} = require('../../db'); +const {hasAccountPermissions, parseAccountSid} = require('./utils'); +const short = require('short-uuid'); +const VoipCarrier = require('../../models/voip-carrier'); +const translator = short(); + let idx = 0; +router.use('/:sid/SpeechCredentials', hasAccountPermissions, require('./speech-credentials')); +router.use('/:sid/RecentCalls', hasAccountPermissions, require('./recent-calls')); +router.use('/:sid/Alerts', hasAccountPermissions, require('./alerts')); +router.use('/:sid/Charges', hasAccountPermissions, require('./charges')); +router.use('/:sid/SipRealms', hasAccountPermissions, require('./sip-realm')); +router.use('/:sid/PredefinedCarriers', hasAccountPermissions, require('./add-from-predefined-carrier')); +router.get('/:sid/Applications', async(req, res) => { + const logger = req.app.locals.logger; + try { + const account_sid = parseAccountSid(req); + const results = await Application.retrieveAll(null, account_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); +router.get('/:sid/VoipCarriers', async(req, res) => { + const logger = req.app.locals.logger; + try { + const account_sid = parseAccountSid(req); + const results = await VoipCarrier.retrieveAll(account_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); +router.post('/:sid/VoipCarriers', async(req, res) => { + const logger = req.app.locals.logger; + const payload = req.body; + try { + const account_sid = parseAccountSid(req); + logger.debug({payload}, 'POST /:sid/VoipCarriers'); + const uuid = await VoipCarrier.make({ + account_sid, + ...payload + }); + res.status(201).json({sid: uuid}); + } catch (err) { + sysError(logger, res, err); + } +}); + function coerceNumbers(callInfo) { if (Array.isArray(callInfo)) { return callInfo.map((ci) => { @@ -59,7 +104,7 @@ function validateUpdateCall(opts) { break; case 2: if (opts.call_hook && opts.child_call_hook) break; - // eslint-disable-next-line no-fallthrough + // eslint-disable-next-line no-fallthrough default: throw new DbErrorBadRequest('multiple options are not allowed in updateCall'); } @@ -167,13 +212,14 @@ async function validateCreateCall(logger, sid, req) { async function validateCreateMessage(logger, sid, req) { const obj = req.body; - const {lookupAccountByPhoneNumber} = req.app.locals; + //const {lookupAccountByPhoneNumber} = req.app.locals; if (req.user.account_sid !== sid) { throw new DbErrorBadRequest(`unauthorized createMessage request for account ${sid}`); } if (!obj.from) throw new DbErrorBadRequest('missing from property'); + /* else { const regex = /^\+(\d+)$/; const arr = regex.exec(obj.from); @@ -181,7 +227,7 @@ async function validateCreateMessage(logger, sid, req) { const account = await lookupAccountByPhoneNumber(from); if (!account) throw new DbErrorBadRequest(`accountSid ${sid} does not own phone number ${from}`); } - + */ if (!obj.to) throw new DbErrorBadRequest('missing to property'); if (!obj.text && !obj.media) { @@ -194,7 +240,7 @@ async function validateAdd(req) { if (req.user.hasAccountAuth) { throw new DbErrorUnprocessableRequest('insufficient permissions to create accounts'); } - if (req.user.hasServiceProviderAuth) { + if (req.user.hasServiceProviderAuth && req.user.service_provider_sid) { /* service providers can only create accounts under themselves */ req.body.service_provider_sid = req.user.service_provider_sid; } @@ -212,6 +258,9 @@ async function validateUpdate(req, sid) { if (req.user.hasAccountAuth && req.user.account_sid !== sid) { throw new DbErrorUnprocessableRequest('insufficient privileges to update this account'); } + if (req.user.hasAccountAuth && req.body.sip_realm) { + throw new DbErrorBadRequest('use POST /Accounts/:sid/sip_realm/:realm to set or change the sip realm'); + } if (req.user.service_provider_sid && !req.user.hasScope('admin')) { const result = await Account.retrieve(sid); @@ -225,8 +274,6 @@ async function validateDelete(req, sid) { if (req.user.hasAccountAuth && req.user.account_sid !== sid) { throw new DbErrorUnprocessableRequest('insufficient privileges to update this account'); } - const assignedPhoneNumbers = await Account.getForeignKeyReferences('phone_numbers.account_sid', sid); - if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete account with phone numbers'); if (req.user.service_provider_sid && !req.user.hasScope('admin')) { const result = await Account.retrieve(sid); if (result[0].service_provider_sid !== req.user.service_provider_sid) { @@ -235,16 +282,16 @@ async function validateDelete(req, sid) { } } -decorate(router, Account, ['delete'], preconditions); /* add */ router.post('/', async(req, res) => { const logger = req.app.locals.logger; try { + const secret = `wh_secret_${translator.generate()}`; await validateAdd(req); // create webhooks if provided - const obj = Object.assign({}, req.body); + const obj = Object.assign({webhook_secret: secret}, req.body); for (const prop of ['registration_hook']) { if (obj[prop]) { obj[`${prop}_sid`] = await Webhook.make(obj[prop]); @@ -252,7 +299,7 @@ router.post('/', async(req, res) => { } } - //logger.debug(`Attempting to add account ${JSON.stringify(obj)}`); + logger.debug(`Attempting to add account ${JSON.stringify(obj)}`); const uuid = await Account.make(obj); res.status(201).json({sid: uuid}); } catch (err) { @@ -287,6 +334,25 @@ router.get('/:sid', async(req, res) => { } }); +router.get('/:sid/WebhookSecret', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = req.user.hasServiceProviderAuth ? req.user.service_provider_sid : null; + const results = await Account.retrieve(req.params.sid, service_provider_sid); + if (results.length === 0) return res.status(404).end(); + let {webhook_secret} = results[0]; + if (req.query.regenerate) { + const secret = `wh_secret_${translator.generate()}`; + await Account.update(req.params.sid, {webhook_secret: secret}); + webhook_secret = secret; + } + return res.status(200).json({webhook_secret}); + } + catch (err) { + sysError(logger, res, err); + } +}); + /* update */ router.put('/:sid', async(req, res) => { const sid = req.params.sid; @@ -342,6 +408,68 @@ router.put('/:sid', async(req, res) => { } }); +/* delete */ +router.delete('/:sid', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + const sqlDeleteGateways = `DELETE from sip_gateways + WHERE voip_carrier_sid IN + (SELECT voip_carrier_sid from voip_carriers where account_sid = ?)`; + try { + await validateDelete(req, sid); + + const [account] = await promisePool.query('SELECT * FROM accounts WHERE account_sid = ?', sid); + const {sip_realm, stripe_customer_id} = account[0]; + /* remove dns records */ + if (process.env.NODE_ENV !== 'test' || process.env.DME_API_KEY) { + + /* retrieve existing dns records */ + const [recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?', sid); + + if (recs.length > 0) { + /* remove existing records from the database and dns provider */ + const arr = /(.*)\.(.*\..*)$/.exec(sip_realm); + if (!arr) throw new DbErrorBadRequest(`invalid sip_realm: ${sip_realm}`); + const domain = arr[2]; + + await promisePool.query('DELETE from dns_records WHERE account_sid = ?', sid); + const deleted = await deleteDnsRecords(logger, domain, recs.map((r) => r.record_id)); + if (!deleted) { + logger.error({recs, sip_realm, sid}, + 'Failed to remove old dns records when changing sip_realm for account'); + } + } + } + + await promisePool.execute('DELETE from api_keys where account_sid = ?', [sid]); + await promisePool.execute( + // eslint-disable-next-line indent +`DELETE from account_products +WHERE account_subscription_sid IN +(SELECT account_subscription_sid FROM +account_subscriptions WHERE account_sid = ?) +`, [sid]); + await promisePool.execute('DELETE from account_subscriptions WHERE account_sid = ?', [sid]); + await promisePool.execute('DELETE from speech_credentials where account_sid = ?', [sid]); + await promisePool.execute('DELETE from users where account_sid = ?', [sid]); + await promisePool.execute('DELETE from phone_numbers where account_sid = ?', [sid]); + await promisePool.execute('DELETE from call_routes where account_sid = ?', [sid]); + await promisePool.execute('DELETE from ms_teams_tenants where account_sid = ?', [sid]); + await promisePool.execute(sqlDeleteGateways, [sid]); + await promisePool.execute('DELETE from voip_carriers where account_sid = ?', [sid]); + await promisePool.execute('DELETE from applications where account_sid = ?', [sid]); + await promisePool.execute('DELETE from accounts where account_sid = ?', [sid]); + + if (stripe_customer_id) { + const response = await deleteCustomer(logger, stripe_customer_id); + logger.info({response}, `deleted stripe customer_id ${stripe_customer_id} for account_si ${sid}`); + } + res.status(204).end(); + } catch (err) { + sysError(logger, res, err); + } +}); + /* retrieve account level api keys */ router.get('/:sid/ApiKeys', async(req, res) => { const logger = req.app.locals.logger; @@ -504,7 +632,7 @@ router.put('/:sid/Calls/:callSid', async(req, res) => { * create a new Message */ router.post('/:sid/Messages', async(req, res) => { - const sid = req.params.sid; + const account_sid = parseAccountSid(req); const setName = `${(process.env.JAMBONES_CLUSTER_ID || 'default')}:active-fs`; const {retrieveSet, logger} = req.app.locals; @@ -516,12 +644,16 @@ router.post('/:sid/Messages', async(req, res) => { } const ip = fs[idx++ % fs.length]; logger.info({fs}, `feature servers available for createMessage API request, selecting ${ip}`); - const serviceUrl = `http://${ip}:3000/v1/createMessage/${sid}`; - await validateCreateMessage(logger, sid, req); + const serviceUrl = `http://${ip}:3000/v1/createMessage/${account_sid}`; + await validateCreateMessage(logger, account_sid, req); - const payload = Object.assign({messageSid: uuidv4(), account_sid: sid}, req.body); + const payload = { + message_sid: uuidv4(), + account_sid, + ...req.body + }; logger.debug({payload}, `sending createMessage API request to to ${ip}`); - updateLastUsed(logger, sid, req).catch((err) => {}); + updateLastUsed(logger, account_sid, req).catch(() => {}); request({ url: serviceUrl, method: 'POST', @@ -534,7 +666,7 @@ router.post('/:sid/Messages', async(req, res) => { } if (response.statusCode !== 200) { logger.error({statusCode: response.statusCode}, `Non-success response returned by createMessage ${serviceUrl}`); - return res.sendStatus(500); + return res.sendStatus(response.statusCode); } res.status(201).json(body); }); diff --git a/lib/routes/api/activation-code.js b/lib/routes/api/activation-code.js new file mode 100644 index 0000000..0d6161c --- /dev/null +++ b/lib/routes/api/activation-code.js @@ -0,0 +1,117 @@ +const router = require('express').Router(); +const debug = require('debug')('jambonz:api-server'); +const {DbErrorBadRequest} = require('../../utils/errors'); +const {promisePool} = require('../../db'); +const {validateEmail, emailSimpleText} = require('../../utils/email-utils'); +const sysError = require('../error'); +const sqlRetrieveUser = `SELECT * from users user +LEFT JOIN accounts AS account +ON user.account_sid = account.account_sid +WHERE user.user_sid = ?`; + +const validateRequest = async(req, res) => { + const payload = req.body || {}; + + /* valid type */ + if (!['email', 'phone'].includes(payload.type)) { + throw new DbErrorBadRequest(`invalid activation type: ${payload.type}`); + } + + /* valid user? */ + const [rows] = await promisePool.query('SELECT * from users WHERE user_sid = ?', + payload.user_sid); + if (0 === rows.length) throw new DbErrorBadRequest('invalid user_sid'); + + /* valid email? */ + if (payload.type === 'email' && !validateEmail(payload.value)) throw new DbErrorBadRequest('invalid email'); + +}; + +router.post('/', async(req, res) => { + const logger = req.app.locals.logger; + const {user_sid, type, code, value} = req.body; + + try { + await validateRequest(req, res); + + const fields = type === 'email' ? + ['email', 'email_validated', 'email_activation_code'] : + ['phone', 'phone_validated', 'phone_activation_code']; + const sql = + `UPDATE users set ${fields[0]} = ?, ${fields[1]} = 0, ${fields[2]} = ? WHERE user_sid = ?`; + const [r] = await promisePool.execute(sql, [value, code, user_sid]); + logger.debug({r}, 'Result from adding activation code'); + debug({r}, 'Result from adding activation code'); + + if (process.env.NODE_ENV !== 'test') { + if (type === 'email') { + /* send code via email */ + const text = ''; + const subject = ''; + await emailSimpleText(logger, value, subject, text); + } + else { + /* send code via SMS */ + } + } + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } +}); + +router.put('/:code', async(req, res) => { + const logger = req.app.locals.logger; + const code = req.params.code; + const {user_sid, type} = req.body; + + try { + let activateAccount = false; + let deactivateOldUsers = false; + let account_sid; + if (type === 'email') { + /* check whether this is first-time activation of account during sign-up/register */ + const [r] = await promisePool.query({sql: sqlRetrieveUser, nestTables: true}, user_sid); + logger.debug({r}, 'activationcode - selected user'); + if (r.length) { + const {user, account} = r[0]; + account_sid = account.account_sid; + const [otherUsers] = await promisePool.query('SELECT * from users WHERE account_sid = ? AND user_sid <> ?', + [account_sid, user_sid]); + logger.debug({otherUsers}, `activationcode - users other than ${user_sid}`); + if (0 === otherUsers.length && user.provider === 'local' && !user.email_validated) { + logger.debug('activationcode - activating account'); + activateAccount = true; + } + else if (otherUsers.length) { + logger.debug('activationcode - adding new user for existing account'); + deactivateOldUsers = true; + } + } + } + const fields = type === 'email' ? + ['email_validated', 'email_activation_code'] : + ['phone_validated', 'phone_activation_code']; + const sql = `UPDATE users set ${fields[0]} = 1, ${fields[1]} = NULL WHERE ${fields[1]} = ? AND user_sid = ?`; + const [r] = await promisePool.execute(sql, [code, user_sid]); + logger.debug({r}, 'Result from validating code'); + debug({r}, 'Result from validating code'); + + if (activateAccount) { + await promisePool.execute('UPDATE accounts SET is_active=1 WHERE account_sid = ?', [account_sid]); + } + else if (deactivateOldUsers) { + const [r] = await promisePool.execute('DELETE FROM users WHERE account_sid = ? AND user_sid <> ?', + [account_sid, user_sid]); + logger.debug({r}, 'Result from deleting old/replaced users'); + } + + if (1 === r.affectedRows) return res.sendStatus(204); + throw new DbErrorBadRequest('invalid user or activation code'); + } catch (err) { + sysError(logger, res, err); + } +}); + + +module.exports = router; diff --git a/lib/routes/api/add-from-predefined-carrier.js b/lib/routes/api/add-from-predefined-carrier.js new file mode 100644 index 0000000..a268837 --- /dev/null +++ b/lib/routes/api/add-from-predefined-carrier.js @@ -0,0 +1,71 @@ +const router = require('express').Router(); +const {DbErrorBadRequest} = require('../../utils/errors'); +const PredefinedCarrier = require('../../models/predefined-carrier'); +const VoipCarrier = require('../../models/voip-carrier'); +const SipGateway = require('../../models/sip-gateway'); +const {parseServiceProviderSid} = require('./utils'); +const {promisePool} = require('../../db'); +const sysError = require('../error'); + +const sqlSelectCarrierByName = `SELECT * FROM voip_carriers +WHERE account_sid = ? +AND name = ?`; +const sqlSelectCarrierByNameForSP = `SELECT * FROM voip_carriers +WHERE service_provider_sid = ? +AND name = ?`; +const sqlSelectTemplateGateways = `SELECT * FROM predefined_sip_gateways +WHERE predefined_carrier_sid = ?`; + + +router.post('/:sid', async(req, res) => { + const logger = req.app.locals.logger; + const {sid } = req.params; + let service_provider_sid; + const {account_sid} = req.user; + if (!account_sid) { + if (!req.user.hasScope('service_provider')) { + logger.error({user: req.user}, 'invalid creds'); + return res.sendStatus(403); + } + service_provider_sid = parseServiceProviderSid(req); + } + try { + const [template] = await PredefinedCarrier.retrieve(sid); + logger.debug({template}, `Retrieved template carrier for sid ${sid}`); + if (!template) return res.sendStatus(404); + + /* make sure not to add the same carrier twice */ + const [r2] = account_sid ? + await promisePool.query(sqlSelectCarrierByName, [account_sid, template.name]) : + await promisePool.query(sqlSelectCarrierByNameForSP, [service_provider_sid, template.name]); + + if (r2.length > 0) { + logger.info({account_sid}, `Failed to add carrier with name ${template.name}, carrier of that name exists`); + throw new DbErrorBadRequest(`A carrier with name ${template.name} already exists`); + } + + /* retrieve all the gateways */ + const [r3] = await promisePool.query(sqlSelectTemplateGateways, template.predefined_carrier_sid); + logger.debug({r3}, `retrieved template gateways for ${template.name}`); + + /* add a voip_carrier */ + // eslint-disable-next-line no-unused-vars + const {requires_static_ip, predefined_carrier_sid, ...obj} = template; + const uuid = await VoipCarrier.make({...obj, account_sid, service_provider_sid}); + + /* add all the gateways */ + for (const gw of r3) { + // eslint-disable-next-line no-unused-vars + const {predefined_carrier_sid, predefined_sip_gateway_sid, ...obj} = gw; + logger.debug({obj}, 'adding gateway'); + await SipGateway.make({...obj, voip_carrier_sid: uuid}); + } + logger.debug({sid: uuid}, 'Successfully added carrier from predefined list'); + res.status(201).json({sid: uuid}); + } catch (err) { + logger.error({err}, 'Error adding voip_carrier from template'); + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/alerts.js b/lib/routes/api/alerts.js new file mode 100644 index 0000000..de9785a --- /dev/null +++ b/lib/routes/api/alerts.js @@ -0,0 +1,35 @@ +const router = require('express').Router(); +const sysError = require('../error'); +const {DbErrorBadRequest} = require('../../utils/errors'); + +const parseAccountSid = (url) => { + const arr = /Accounts\/([^\/]*)/.exec(url); + if (arr) return arr[1]; +}; + +router.get('/', async(req, res) => { + const {logger, queryAlerts} = req.app.locals; + try { + logger.debug({opts: req.query}, 'GET /Alerts'); + const account_sid = parseAccountSid(req.originalUrl); + const {page, count, alert_type, days, start, end} = req.query || {}; + if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg'); + if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg'); + + const data = await queryAlerts({ + account_sid, + page, + page_size: count, + alert_type, + days, + start: days ? undefined : start, + end: days ? undefined : end, + }); + + res.status(200).json(data); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/api-keys.js b/lib/routes/api/api-keys.js index 1a2e935..209654d 100644 --- a/lib/routes/api/api-keys.js +++ b/lib/routes/api/api-keys.js @@ -5,7 +5,7 @@ const Account = require('../../models/account'); const decorate = require('./decorate'); const uuidv4 = require('uuid/v4'); const assert = require('assert'); -const sysError = require('./error'); +const sysError = require('../error'); const preconditions = { 'add': validateAddToken, 'delete': validateDeleteToken diff --git a/lib/routes/api/applications.js b/lib/routes/api/applications.js index 122f80c..99a0b06 100644 --- a/lib/routes/api/applications.js +++ b/lib/routes/api/applications.js @@ -4,7 +4,7 @@ const Application = require('../../models/application'); const Account = require('../../models/account'); const Webhook = require('../../models/webhook'); const decorate = require('./decorate'); -const sysError = require('./error'); +const sysError = require('../error'); const preconditions = { 'add': validateAdd, 'update': validateUpdate, @@ -33,8 +33,11 @@ async function validateAdd(req) { } async function validateUpdate(req, sid) { - if (req.user.account_sid && sid !== req.user.account_sid) { - throw new DbErrorBadRequest('you may not update or delete an application associated with a different account'); + if (req.user.account_sid) { + const app = await Application.retrieve(sid); + if (!app || !app.length || app[0].account_sid !== req.user.account_sid) { + throw new DbErrorBadRequest('you may not update or delete an application associated with a different account'); + } } if (req.body.call_hook && typeof req.body.call_hook !== 'object') { throw new DbErrorBadRequest('\'call_hook\' must be an object when updating an application'); @@ -45,8 +48,12 @@ async function validateUpdate(req, sid) { } async function validateDelete(req, sid) { - if (req.user.account_sid && sid !== req.user.account_sid) { - throw new DbErrorBadRequest('you may not update or delete an application associated with a different account'); + if (req.user.hasAccountAuth) { + const result = await Application.retrieve(sid); + if (!result || 0 === result.length) throw new DbErrorBadRequest('application does not exist'); + if (result[0].account_sid !== req.user.account_sid) { + throw new DbErrorUnprocessableRequest('cannot delete application owned by a different account'); + } } const assignedPhoneNumbers = await Application.getForeignKeyReferences('phone_numbers.application_sid', sid); if (assignedPhoneNumbers > 0) throw new DbErrorUnprocessableRequest('cannot delete application with phone numbers'); diff --git a/lib/routes/api/availability.js b/lib/routes/api/availability.js new file mode 100644 index 0000000..89bc602 --- /dev/null +++ b/lib/routes/api/availability.js @@ -0,0 +1,31 @@ +const router = require('express').Router(); +const {DbErrorBadRequest} = require('../../utils/errors'); +const {promisePool} = require('../../db'); +const sysError = require('../error'); + + +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const {type, value} = req.query; + + try { + + if (['email', 'phone'].includes(type)) { + const field = type === 'email' ? 'email' : 'phone'; + const sql = `SELECT * from users WHERE ${field} = ?`; + const [r] = await promisePool.execute(sql, [value]); + res.json({available: 0 === r.length}); + } + else if (type === 'subdomain') { + const sql = 'SELECT * from accounts WHERE sip_realm = ?'; + const [r] = await promisePool.execute(sql, [value]); + res.json({available: 0 === r.length}); + } + else throw new DbErrorBadRequest(`invalid type: ${type}`); + } catch (err) { + sysError(logger, res, err); + } +}); + + +module.exports = router; diff --git a/lib/routes/api/beta-invite-codes.js b/lib/routes/api/beta-invite-codes.js new file mode 100644 index 0000000..0fcdbaa --- /dev/null +++ b/lib/routes/api/beta-invite-codes.js @@ -0,0 +1,32 @@ +const router = require('express').Router(); +const sysError = require('../error'); +const {promisePool} = require('../../db'); +const short = require('short-uuid'); +const translator = short('0123456789ABCXZ'); + +router.post('/', async(req, res) => { + const {logger} = req.app.locals; + logger.debug({payload: req.body}, 'POST /BetaInviteCodes'); + try { + const {count} = req.body || {}; + const total = Math.max(count || 1, 1); + const codes = []; + let added = 0; + while (added < total) { + const code = translator.new().substring(0, 6); + if (!codes.find((c) => c === code)) { + codes.push(code); + added++; + } + } + + const values = codes.map((c) => `('${c}')`).join(','); + const sql = `INSERT INTO beta_invite_codes (invite_code) VALUES ${values}`; + const [r] = await promisePool.query(sql); + res.status(200).json({status: 'ok', added: r.affectedRows, codes}); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/change-password.js b/lib/routes/api/change-password.js new file mode 100644 index 0000000..ccebbf8 --- /dev/null +++ b/lib/routes/api/change-password.js @@ -0,0 +1,47 @@ +const router = require('express').Router(); +//const debug = require('debug')('jambonz:api-server'); +const {DbErrorBadRequest} = require('../../utils/errors'); +const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils'); +const {promisePool} = require('../../db'); +const sysError = require('../error'); +const sqlUpdatePassword = `UPDATE users +SET hashed_password= ? +WHERE user_sid = ?`; + +router.post('/', async(req, res) => { + const {logger, retrieveKey, deleteKey} = req.app.locals; + const {user_sid} = req.user; + const {old_password, new_password} = req.body; + try { + if (!old_password || !new_password) throw new DbErrorBadRequest('missing old_password or new_password'); + + /* validate existing password */ + { + const [r] = await promisePool.query('SELECT * from users where user_sid = ?', user_sid); + logger.debug({user: [r[0]]}, 'change password for user'); + + if (r[0].provider !== 'local') { + throw new DbErrorBadRequest('user is using oauth authentication'); + } + + const isCorrect = await verifyPassword(r[0].hashed_password, old_password); + if (!isCorrect) { + const key = `reset-link:${old_password}`; + const user_sid = await retrieveKey(key); + if (!user_sid) throw new DbErrorBadRequest('old_password is incorrect'); + await deleteKey(key); + } + } + + /* store new password */ + const passwordHash = await generateHashedPassword(new_password); + const [r] = await promisePool.execute(sqlUpdatePassword, [passwordHash, user_sid]); + if (r.affectedRows !== 1) throw new Error('failed to update user with new password'); + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + return; + } +}); + +module.exports = router; diff --git a/lib/routes/api/charges.js b/lib/routes/api/charges.js new file mode 100644 index 0000000..d33f113 --- /dev/null +++ b/lib/routes/api/charges.js @@ -0,0 +1,46 @@ +const router = require('express').Router(); +const sysError = require('../error'); +//const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors'); + + +/** + * retrieve charges for an account and/or call + */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + try { + res.status(200).json([ + { + charge_sid: 'f8d2a604-ed29-4eac-9efc-8f58b0e438ca', + account_sid: req.user.account_sid, + call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1', + billed_at: '2020-01-01 15:10:10', + billed_activity: 'outbound-call', + call_secs_billed: 392, + amount_charged: 0.0200 + }, + { + charge_sid: 'd9659f3f-3a94-455c-9e8e-3b36f250ffc8', + account_sid: req.user.account_sid, + call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1', + billed_at: '2020-01-01 15:10:10', + billed_activity: 'tts', + tts_chars_billed: 100, + amount_charged: 0.0130 + }, + { + charge_sid: 'adcc1e79-eb79-4370-ab74-4c2e9a41339a', + account_sid: req.user.account_sid, + call_billing_record_sid: 'e4be80a4-6597-49cf-8605-6b94493fada1', + billed_at: '2020-01-01 15:10:10', + billed_activity: 'stt', + stt_secs_billed: 30, + amount_charged: 0.0015 + } + ]); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/decorate.js b/lib/routes/api/decorate.js index 470f576..a3f7c8e 100644 --- a/lib/routes/api/decorate.js +++ b/lib/routes/api/decorate.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const sysError = require('./error'); +const sysError = require('../error'); module.exports = decorate; @@ -58,7 +58,7 @@ function retrieve(router, klass) { const logger = req.app.locals.logger; try { const results = await klass.retrieve(req.params.sid); - if (results.length === 0) return res.status(404).end(); + if (results.length === 0) return res.sendStatus(404); return res.status(200).json(results[0]); } catch (err) { @@ -78,7 +78,7 @@ function update(router, klass, preconditions) { } const rowsAffected = await klass.update(sid, req.body); if (rowsAffected === 0) { - return res.status(404).end(); + return res.sendStatus(404); } res.status(204).end(); } catch (err) { @@ -99,9 +99,9 @@ function remove(router, klass, preconditions) { const rowsAffected = await klass.remove(sid); if (rowsAffected === 0) { logger.info(`unable to delete ${klass.name} with sid ${sid}: not found`); - return res.status(404).end(); + return res.sendStatus(404); } - res.status(204).end(); + res.sendStatus(204); } catch (err) { sysError(logger, res, err); } diff --git a/lib/routes/api/forgot-password.js b/lib/routes/api/forgot-password.js new file mode 100644 index 0000000..e09de20 --- /dev/null +++ b/lib/routes/api/forgot-password.js @@ -0,0 +1,82 @@ +const router = require('express').Router(); +//const debug = require('debug')('jambonz:api-server'); +const short = require('short-uuid'); +const translator = short(); +const {validateEmail, emailSimpleText} = require('../../utils/email-utils'); +const {promisePool} = require('../../db'); +const sysError = require('../error'); +const sql = `SELECT * from users user +LEFT JOIN accounts AS acc +ON acc.account_sid = user.account_sid +WHERE user.email = ?`; + +function createOauthEmailText(provider) { + return `Hi there! + + Someone (presumably you!) requested to reset their password. + However, the account associated with this email is using oauth identification via ${provider}, + Please change your password through that provider, if you wish to. + + If you did not make this request, please delete this email. No further action is required. + + Best, + + Jambonz support team`; +} + +function createResetEmailText(link) { + const baseUrl = 'http://localhost:3001'; + + return `Hi there! + + Someone (presumably you!) requested to reset their password. + Please follow the link below to reset your password: + + ${baseUrl}/reset-password/${link} + + This link is valid for 1 hour only. + If you did not make this request, please delete this email. No further action is required. + + Best, + + Jambonz support team`; +} + +router.post('/', async(req, res) => { + const {logger, addKey} = req.app.locals; + const {email} = req.body; + let obj; + try { + if (!email || !validateEmail(email)) { + return res.status(400).json({error: 'invalid or missing email'}); + } + + const [r] = await promisePool.query({sql, nestTables: true}, email); + if (0 === r.length) { + return res.status(400).json({error: 'email does not exist'}); + } + obj = r[0]; + if (!obj.acc.is_active) { + return res.status(400).json({error: 'you may not reset the password of an inactive account'}); + } + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + return; + } + + if (obj.user.provider !== 'local') { + /* send email indicating they need to change via their oauth provider */ + emailSimpleText(logger, email, 'Reset password request', createOauthEmailText(obj.user.provider)); + + } + else { + /* generate a link for this user to reset, send email */ + const link = translator.generate(); + addKey(`reset-link:${link}`, obj.user.user_sid, 3600) + .catch((err) => logger.error({err}, 'Error adding reset link to redis')); + emailSimpleText(logger, email, 'Reset password request', createResetEmailText(link)); + } +}); + +module.exports = router; diff --git a/lib/routes/api/index.js b/lib/routes/api/index.js index 2302e5b..64a71f5 100644 --- a/lib/routes/api/index.js +++ b/lib/routes/api/index.js @@ -1,26 +1,45 @@ const api = require('express').Router(); -function isAdminScope(req, res, next) { +const isAdminScope = (req, res, next) => { if (req.user.hasScope('admin')) return next(); res.status(403).json({ status: 'fail', message: 'insufficient privileges' }); -} +}; +api.use('/BetaInviteCodes', isAdminScope, require('./beta-invite-codes')); api.use('/ServiceProviders', isAdminScope, require('./service-providers')); -api.use('/VoipCarriers', isAdminScope, require('./voip-carriers')); -api.use('/SipGateways', isAdminScope, require('./sip-gateways')); -api.use('/PhoneNumbers', isAdminScope, require('./phone-numbers')); +api.use('/VoipCarriers', require('./voip-carriers')); +api.use('/Webhooks', require('./webhooks')); +api.use('/SipGateways', require('./sip-gateways')); +api.use('/SmppGateways', require('./smpp-gateways')); +api.use('/PhoneNumbers', require('./phone-numbers')); api.use('/ApiKeys', require('./api-keys')); api.use('/Accounts', require('./accounts')); api.use('/Applications', require('./applications')); api.use('/MicrosoftTeamsTenants', require('./tenants')); -api.use('/Sbcs', isAdminScope, require('./sbcs')); +api.use('/Sbcs', require('./sbcs')); api.use('/Users', require('./users')); +api.use('/register', require('./register')); +api.use('/signin', require('./signin')); api.use('/login', require('./login')); +api.use('/logout', require('./logout')); +api.use('/forgot-password', require('./forgot-password')); +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('/Products', require('./products')); +api.use('/Prices', require('./prices')); +api.use('/StripeCustomerId', require('./stripe-customer-id')); +api.use('/Subscriptions', require('./subscriptions')); +api.use('/Invoices', require('./invoices')); +api.use('/InviteCodes', require('./invite-codes')); +api.use('/PredefinedCarriers', require('./predefined-carriers')); // messaging +api.use('/Smpps', require('./smpps')); // our smpp server info api.use('/messaging', require('./sms-inbound')); // inbound SMS from carrier api.use('/outboundSMS', require('./sms-outbound')); // outbound SMS from feature server diff --git a/lib/routes/api/invite-codes.js b/lib/routes/api/invite-codes.js new file mode 100644 index 0000000..f6a63db --- /dev/null +++ b/lib/routes/api/invite-codes.js @@ -0,0 +1,34 @@ +const router = require('express').Router(); +const sysError = require('../error'); +const {promisePool} = require('../../db'); +const sqlClaim = `UPDATE beta_invite_codes +SET in_use = 1 +WHERE invite_code = ? +AND in_use = 0`; +const sqlTest = `SELECT * FROM beta_invite_codes +WHERE invite_code = ? +AND in_use = 0`; +router.post('/', async(req, res) => { + const {logger} = req.app.locals; + try { + const {code, test} = req.body; + logger.debug({code}, 'POST /InviteCodes'); + if ('test' === process.env.NODE_ENV) { + if (code.endsWith('0')) return res.sendStatus(404); + res.sendStatus(204); + return; + } + if (test) { + const [r] = await promisePool.execute(sqlTest, [code]); + res.sendStatus(1 === r.length ? 204 : 404); + } + else { + const [r] = await promisePool.execute(sqlClaim, [code]); + res.sendStatus(1 === r.affectedRows ? 204 : 404); + } + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/invoices.js b/lib/routes/api/invoices.js new file mode 100644 index 0000000..01e71f9 --- /dev/null +++ b/lib/routes/api/invoices.js @@ -0,0 +1,26 @@ +const router = require('express').Router(); +const assert = require('assert'); +const Account = require('../../models/account'); +const { + retrieveUpcomingInvoice +} = require('../../utils/stripe-utils'); +const sysError = require('../error'); + +/* retrieve */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid} = req.user; + try { + const results = await Account.retrieve(account_sid); + assert.ok(1 === results.length, `account ${account_sid} not found`); + const {stripe_customer_id} = results[0]; + if (!stripe_customer_id) return res.sendStatus(404); + const invoice = await retrieveUpcomingInvoice(logger, stripe_customer_id); + res.status(200).json(invoice); + } catch (err) { + if (err.statusCode) return res.sendStatus(err.statusCode); + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/login.js b/lib/routes/api/login.js index 64df719..4d61432 100644 --- a/lib/routes/api/login.js +++ b/lib/routes/api/login.js @@ -1,19 +1,10 @@ const router = require('express').Router(); -const crypto = require('crypto'); const {getMysqlConnection} = require('../../db'); +const {verifyPassword} = require('../../utils/password-utils'); const retrieveSql = 'SELECT * from users where name = ?'; const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL'; -const sha512 = function(password, salt) { - const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */ - hash.update(password); - var value = hash.digest('hex'); - return { - salt:salt, - passwordHash:value - }; -}; router.post('/', (req, res) => { const logger = req.app.locals.logger; @@ -28,7 +19,7 @@ router.post('/', (req, res) => { logger.error({err}, 'Error getting db connection'); return res.sendStatus(500); } - conn.query(retrieveSql, [username], (err, results) => { + conn.query(retrieveSql, [username], async(err, results) => { conn.release(); if (err) { logger.error({err}, 'Error getting db connection'); @@ -40,14 +31,10 @@ router.post('/', (req, res) => { } logger.info({results}, 'successfully retrieved account'); - const salt = results[0].salt; - const trueHash = results[0].hashed_password; - const forceChange = results[0].force_change; + const isCorrect = await verifyPassword(results[0].hashed_password, password); + if (!isCorrect) return res.sendStatus(403); - const {passwordHash} = sha512(password, salt); - if (trueHash !== passwordHash) return res.sendStatus(403); - - if (forceChange) return res.json({user_sid: results[0].user_sid, force_change: true}); + const force_change = !!results[0].force_change; getMysqlConnection((err, conn) => { if (err) { @@ -64,7 +51,7 @@ router.post('/', (req, res) => { logger.error('Database has no admin token provisioned...run reset_admin_password'); return res.sendStatus(500); } - res.json({user_sid: results[0].user_sid, token: tokenResults[0].token}); + res.json({user_sid: results[0].user_sid, force_change, token: tokenResults[0].token}); }); }); }); diff --git a/lib/routes/api/logout.js b/lib/routes/api/logout.js new file mode 100644 index 0000000..1ac746e --- /dev/null +++ b/lib/routes/api/logout.js @@ -0,0 +1,23 @@ +const router = require('express').Router(); +const debug = require('debug')('jambonz:api-server'); +const {hashString} = require('../../utils/password-utils'); +const sysError = require('../error'); + +router.post('/', async(req, res) => { + const {logger, addKey} = req.app.locals; + const {jwt} = req.user; + + debug(`adding jwt to blacklist: ${jwt}`); + + try { + /* add key to blacklist */ + const s = `jwt:${hashString(jwt)}`; + const result = await addKey(s, '1', 3600); + debug(`result from adding ${s}: ${result}`); + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/phone-numbers.js b/lib/routes/api/phone-numbers.js index b4e8c91..9cca699 100644 --- a/lib/routes/api/phone-numbers.js +++ b/lib/routes/api/phone-numbers.js @@ -3,13 +3,14 @@ const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/er const PhoneNumber = require('../../models/phone-number'); const VoipCarrier = require('../../models/voip-carrier'); const decorate = require('./decorate'); -const validateNumber = require('../../utils/phone-number-syntax'); +const {e164} = require('../../utils/phone-number-utils'); const preconditions = { 'add': validateAdd, 'delete': checkInUse, 'update': validateUpdate }; -const sysError = require('./error'); +const sysError = require('../error'); + /* check for required fields when adding */ async function validateAdd(req) { @@ -19,17 +20,19 @@ async function validateAdd(req) { req.body.account_sid = req.user.account_sid; } - if (!req.body.voip_carrier_sid) throw new DbErrorBadRequest('voip_carrier_sid is required'); if (!req.body.number) throw new DbErrorBadRequest('number is required'); - validateNumber(req.body.number); + const formattedNumber = e164(req.body.number); + req.body.number = formattedNumber; } catch (err) { throw new DbErrorBadRequest(err.message); } /* check that voip carrier exists */ - const result = await VoipCarrier.retrieve(req.body.voip_carrier_sid); - if (!result || result.length === 0) { - throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`); + if (req.body.voip_carrier_sid) { + const result = await VoipCarrier.retrieve(req.body.voip_carrier_sid); + if (!result || result.length === 0) { + throw new DbErrorBadRequest(`voip_carrier not found for sid ${req.body.voip_carrier_sid}`); + } } } @@ -37,7 +40,7 @@ async function validateAdd(req) { async function checkInUse(req, sid) { const phoneNumber = await PhoneNumber.retrieve(sid); if (req.user.hasAccountAuth) { - if (phoneNumber.account_sid !== req.user.account_sid) { + if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) { throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account'); } } @@ -53,10 +56,16 @@ async function validateUpdate(req, sid) { const phoneNumber = await PhoneNumber.retrieve(sid); if (req.user.hasAccountAuth) { - if (phoneNumber.account_sid !== req.user.account_sid) { - throw new DbErrorUnprocessableRequest('cannot delete a phone number that belongs to another account'); + if (phoneNumber && phoneNumber.length && phoneNumber[0].account_sid !== req.user.account_sid) { + throw new DbErrorUnprocessableRequest('cannot operate on a phone number that belongs to another account'); } } + + // TODO: if we are assigning to an account, verify it exists + + // TODO: if we are assigning to an application, verify it is associated to the same account + + // TODO: if we are removing from an account, verify we are also removing from application. } decorate(router, PhoneNumber, ['add', 'update', 'delete'], preconditions); @@ -86,5 +95,4 @@ router.get('/:sid', async(req, res) => { } }); - module.exports = router; diff --git a/lib/routes/api/predefined-carriers.js b/lib/routes/api/predefined-carriers.js new file mode 100644 index 0000000..32d77aa --- /dev/null +++ b/lib/routes/api/predefined-carriers.js @@ -0,0 +1,7 @@ +const router = require('express').Router(); +const PredefinedCarrier = require('../../models/predefined-carrier'); +const decorate = require('./decorate'); + +decorate(router, PredefinedCarrier, ['list']); + +module.exports = router; diff --git a/lib/routes/api/prices.js b/lib/routes/api/prices.js new file mode 100644 index 0000000..d198721 --- /dev/null +++ b/lib/routes/api/prices.js @@ -0,0 +1,108 @@ +const router = require('express').Router(); +const Product = require('../../models/product'); +const {promisePool} = require('../../db'); +const sysError = require('../error'); +const sqlRetrieveSpecialOffers = `SELECT * +FROM account_offers offer +LEFT JOIN products AS product ON product.product_sid = offer.product_sid +WHERE offer.account_sid = ?`; + +const combineProductAndPrice = (localProducts, product, prices) => { + const lp = localProducts.find((lp) => lp.category === product.metadata.jambonz_category); + return { + product_sid: lp.product_sid, + name: lp.name, + category: lp.category, + stripe_product_id: product.id, + description: product.description, + unit_label: product.unit_label, + prices: prices.map((price) => { + return { + stripe_price_id: price.id, + billing_scheme: price.billing_scheme, + currency: price.currency, + recurring: price.recurring, + tiers_mode: price.tiers_mode, + tiers: price.tiers, + type: price.type, + unit_amount: price.unit_amount, + unit_amount_decimal: price.unit_amount_decimal + }; + }) + }; +}; + + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid} = req.user || {}; + const {listProducts, retrieveProduct, retrievePricesForProduct} = require('../../utils/stripe-utils'); + try { + const localProducts = await Product.retrieveAll(); + + /** + * If this request is for a specific account (we have an account_sid) + * then check to see if we have any special offers for this account + */ + const selectedProducts = []; + if (account_sid) { + const [r] = await promisePool.query({sql: sqlRetrieveSpecialOffers, nestTables: true}, account_sid); + logger.debug({r}, `retrieved special offer ids for account_sid ${account_sid}`); + + if (r.length > 0) { + /* retrieve all the offers for this account */ + const products = await Promise.all(r.map((row) => retrieveProduct(logger, row.offer.stripe_product_id))); + logger.debug({products}, `retrieved special offer products for account_sid ${account_sid}`); + const prices = await Promise.all(products.map((prod) => retrievePricesForProduct(logger, prod.id))); + logger.debug({prices}, `retrieved special offer prices for account_sid ${account_sid}`); + + for (let i = 0; i < products.length; i++) { + selectedProducts.push(combineProductAndPrice(localProducts, products[i], prices[i].data)); + } + } + } + + /** + * we must return at least pricing for sessions and devices, so find and use + * the general pricing if no account-specific product was specified for these + */ + const haveSessionPricing = selectedProducts.find((prod) => prod.category === 'voice_call_session'); + const haveDevicePricing = selectedProducts.find((prod) => prod.category === 'device'); + + if (haveSessionPricing && haveDevicePricing) { + logger.debug({selectedProducts}, 'found account level offers for sessions and devices'); + return res.status(200).json(selectedProducts); + } + + /* need to get default pricing */ + const allProducts = await listProducts(logger); + logger.debug({allProducts}, 'retrieved all products'); + const defaultProducts = allProducts.data.filter((prod) => + ['voice_call_session', 'device'].includes(prod.metadata.jambonz_category) && + 'general' === prod.metadata.availability); + logger.debug({defaultProducts}, 'default products'); + + if (!haveSessionPricing) { + const product = defaultProducts.find((prod) => 'voice_call_session' === prod.metadata.jambonz_category); + if (product) { + logger.debug(`retrieving prices for product id ${product.id}`); + const prices = await retrievePricesForProduct(logger, product.id); + selectedProducts.push(combineProductAndPrice(localProducts, product, prices.data)); + } + } + if (!haveDevicePricing) { + const product = defaultProducts.find((prod) => 'device' === prod.metadata.jambonz_category); + if (product) { + logger.debug(`retrieving prices for product id ${product.id}`); + const prices = await retrievePricesForProduct(logger, product.id); + selectedProducts.push(combineProductAndPrice(localProducts, product, prices.data)); + } + } + res.status(200).json(selectedProducts); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/products.js b/lib/routes/api/products.js new file mode 100644 index 0000000..e586d18 --- /dev/null +++ b/lib/routes/api/products.js @@ -0,0 +1,78 @@ +const assert = require('assert'); +const router = require('express').Router(); +const Product = require('../../models/product'); +const {listProducts, listPrices} = require('../../utils/stripe-utils'); +const sysError = require('./error'); + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + try { + const [stripeProducts, localProducts] = await Promise.all([listProducts(), Product.retrieveAll()]); + console.log(stripeProducts); + console.log(localProducts); + const arr = localProducts.map((p) => { + const stripe = stripeProducts.data + .find((s) => s.metadata.jambonz_category === p.category); + assert.ok(stripe, `No stripe product found for category ${p.category}`); + Object.assign(p, { + stripe_product_id: stripe.id, + statement_descriptor: stripe.statement_descriptor, + description: stripe.description, + unit_label: stripe.unit_label + }); + return p; + }); + res.status(200).json(arr); + } catch (err) { + sysError(logger, res, err); + } +}); + +/* get */ +router.get('/:sid', async(req, res) => { + const logger = req.app.locals.logger; + try { + const [allPrices, results] = await Promise.all([listPrices(), Product.retrieve(req.params.sid)]); + if (results.length === 0) return res.sendStatus(404); + const product = results[0]; + const prices = allPrices.data + .filter((p) => p.active && p.product.active) + .filter((p) => p.product.metadata.jambonz_category === product.category); + assert(prices.length > 0, `No pricing data found for product ${req.params.sid}`); + const stripe = prices[0].product; + Object.assign(product, { + stripe_product_id: stripe.id, + statement_descriptor: stripe.statement_descriptor, + description: stripe.description, + unit_label: stripe.unit_label + }); + + // get pricing + Object.assign(product, { + pricing: { + billing_scheme: stripe.billing_scheme, + type: prices[0].type + } + }); + + product.pricing.fees = prices.map((price) => { + const obj = { + stripe_price_id: price.id, + currency: price.currency, + unit_amount: price.unit_amount, + unit_amount_decimal: price.unit_amount_decimal + }; + if (price.tiers) { + obj.tiers = price.tiers; + obj.tiers_mode = price.tiers_mode; + } + return obj; + }); + res.status(200).json(product); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/recent-calls.js b/lib/routes/api/recent-calls.js new file mode 100644 index 0000000..85a720b --- /dev/null +++ b/lib/routes/api/recent-calls.js @@ -0,0 +1,37 @@ +const router = require('express').Router(); +const sysError = require('../error'); +const {DbErrorBadRequest} = require('../../utils/errors'); + +const parseAccountSid = (url) => { + const arr = /Accounts\/([^\/]*)/.exec(url); + if (arr) return arr[1]; +}; + +router.get('/', async(req, res) => { + const {logger, queryCdrs} = req.app.locals; + try { + logger.debug({opts: req.query}, 'GET /RecentCalls'); + const account_sid = parseAccountSid(req.originalUrl); + const {page, count, trunk, direction, days, answered, start, end} = req.query || {}; + if (!page || page < 1) throw new DbErrorBadRequest('missing or invalid "page" query arg'); + if (!count || count < 25 || count > 500) throw new DbErrorBadRequest('missing or invalid "count" query arg'); + + const data = await queryCdrs({ + account_sid, + page, + page_size: count, + trunk, + direction, + days, + answered, + start: days ? undefined : start, + end: days ? undefined : end, + }); + + res.status(200).json(data); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/register.js b/lib/routes/api/register.js new file mode 100644 index 0000000..3edf799 --- /dev/null +++ b/lib/routes/api/register.js @@ -0,0 +1,344 @@ +const router = require('express').Router(); +const debug = require('debug')('jambonz:api-server'); +const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); +const {promisePool} = require('../../db'); +const {doGithubAuth, doGoogleAuth, doLocalAuth} = require('../../utils/oauth-utils'); +const {validateEmail} = require('../../utils/email-utils'); +const uuid = require('uuid').v4; +const short = require('short-uuid'); +const translator = short(); +const jwt = require('jsonwebtoken'); +const {setupFreeTrial, createTestCdrs, createTestAlerts} = require('./utils'); +const {generateHashedPassword} = require('../../utils/password-utils'); +const sysError = require('../error'); +const insertUserSql = `INSERT into users +(user_sid, account_sid, name, email, provider, provider_userid, email_validated) +values (?, ?, ?, ?, ?, ?, 1)`; +const insertUserLocalSql = `INSERT into users +(user_sid, account_sid, name, email, email_activation_code, email_validated, provider, hashed_password) +values (?, ?, ?, ?, ?, 0, 'local', ?)`; +const insertAccountSql = `INSERT into accounts +(account_sid, service_provider_sid, name, is_active, webhook_secret, trial_end_date) +values (?, ?, ?, ?, ?, CURDATE() + INTERVAL 21 DAY)`; +const queryRootDomainSql = `SELECT root_domain +FROM service_providers +WHERE service_providers.service_provider_sid = ?`; +const insertSignupHistorySql = `INSERT into signup_history +(email, name) +values (?, ?)`; + +const addLocalUser = async(logger, user_sid, account_sid, + name, email, email_activation_code, passwordHash) => { + const [r] = await promisePool.execute(insertUserLocalSql, + [ + user_sid, + account_sid, + name, + email, + email_activation_code, + passwordHash + ]); + debug({r}, 'Result from adding user'); +}; +const addOauthUser = async(logger, user_sid, account_sid, + name, email, provider, provider_userid) => { + const [r] = await promisePool.execute(insertUserSql, + [ + user_sid, + account_sid, + name, + email, + provider, + provider_userid + ]); + logger.debug({r}, 'Result from adding user'); +}; + +const validateRequest = async(req, user_sid) => { + const payload = req.body || {}; + + /* check required properties are there */ + ['provider', 'service_provider_sid'].forEach((prop) => { + if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop}`); + }); + + /* valid service provider? */ + const [rows] = await promisePool.query('SELECT * from service_providers WHERE service_provider_sid = ?', + payload.service_provider_sid); + if (0 === rows.length) throw new DbErrorUnprocessableRequest('invalid service_provider_sid'); + + /* valid provider? */ + if (!['local', 'github', 'google', 'twitter'].includes(payload.provider)) { + throw new DbErrorUnprocessableRequest(`invalid provider: ${payload.provider}`); + } + + /* if local provider then email/password */ + if ('local' === payload.provider) { + if (!payload.email || !payload.password) throw new DbErrorBadRequest('missing email or password'); + + /* valid email? */ + if (!validateEmail(payload.email)) throw new DbErrorBadRequest('invalid email'); + + /* valid password? */ + if (payload.password.length < 6) throw new DbErrorBadRequest('password must be at least 6 characters'); + + /* is this email available? */ + if (user_sid) { + const [rows] = await promisePool.query('SELECT * from users WHERE email = ? AND user_sid <> ?', + [payload.email, user_sid]); + if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email'); + } + else { + const [rows] = await promisePool.query('SELECT * from users WHERE email = ?', payload.email); + if (rows.length > 0) throw new DbErrorUnprocessableRequest('account already exists for this email'); + } + + /* verify that we have a code to email them */ + if (!payload.email_activation_code) throw new DbErrorBadRequest('email activation code required'); + } + else { + ['oauth2_code', 'oauth2_state', 'oauth2_client_id', 'oauth2_redirect_uri'].forEach((prop) => { + if (!payload[prop]) throw new DbErrorBadRequest(`missing ${prop} for provider ${payload.provider}`); + }); + } +}; + +const parseAuthorizationToken = (logger, req) => { + const notfound = {}; + const authHeader = req.get('Authorization'); + if (!authHeader) return Promise.resolve(notfound); + + return new Promise((resolve) => { + const arr = /^Bearer (.*)$/.exec(req.get('Authorization')); + if (!arr) return resolve(notfound); + jwt.verify(arr[1], process.env.JWT_SECRET, async(err, decoded) => { + if (err) return resolve(notfound); + logger.debug({jwt: decoded}, 'register - create new user for existing account'); + resolve(decoded); + }); + }); +}; + +/** + * called to create a new user and account + * or new user with existing account, in case of "change auth mechanism" + */ +router.post('/', async(req, res) => { + const {logger, writeCdrs, writeAlerts, AlertType} = req.app.locals; + const userProfile = {}; + + try { + const {user_sid, account_sid} = await parseAuthorizationToken(logger, req); + await validateRequest(req, user_sid); + + logger.debug({payload: req.body}, 'POST /register'); + + if (req.body.provider === 'github') { + const user = await doGithubAuth(logger, req.body); + logger.info({user}, 'retrieved user details from github'); + Object.assign(userProfile, { + name: user.name, + email: user.email, + email_validated: user.email_validated, + avatar_url: user.avatar_url, + provider: 'github', + provider_userid: user.login + }); + } + else if (req.body.provider === 'google') { + const user = await doGoogleAuth(logger, req.body); + logger.info({user}, 'retrieved user details from google'); + Object.assign(userProfile, { + name: user.name, + email: user.email, + email_validated: user.verified_email, + picture: user.picture, + provider: 'google', + provider_userid: user.id + }); + } + else if (req.body.provider === 'local') { + const user = await doLocalAuth(logger, req.body); + logger.info({user}, 'retrieved user details for local provider'); + debug({user}, 'retrieved user details for local provider'); + Object.assign(userProfile, { + name: user.name, + email: user.email, + provider: 'local', + email_activation_code: user.email_activation_code + }); + } + + if (req.body.provider !== 'local') { + /* when using oauth2, check to see if user already exists */ + const [users] = await promisePool.query( + 'SELECT * from users WHERE provider = ? AND provider_userid = ?', + [userProfile.provider, userProfile.provider_userid]); + logger.debug({users}, `Result from retrieving user for ${userProfile.provider}:${userProfile.provider_userid}`); + if (1 === users.length) { + + /* if changing existing account to oauth, no other user with that provider/userid must exist */ + if (user_sid) { + throw new DbErrorUnprocessableRequest('account already exists for this oauth user/provider'); + } + Object.assign(userProfile, { + user_sid: users[0].user_sid, + account_sid: users[0].account_sid, + name: users[0].name, + email: users[0].email, + phone: users[0].phone, + pristine: false, + email_validated: users[0].email_validated ? true : false, + phone_validated: users[0].phone_validated ? true : false, + scope: users[0].scope + }); + + const [accounts] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', + userProfile.account_sid); + if (accounts.length === 0) throw new DbErrorUnprocessableRequest('user exists with no associated account'); + Object.assign(userProfile, { + is_active: accounts[0].is_active == 1, + tutorial_completion: accounts[0].tutorial_completion + }); + } + else { + /* you can not register from the sign-in page */ + if (req.body.locationBeforeAuth === '/sign-in') { + logger.debug('redirecting user to /register so they accept Ts & Cs'); + return res.status(404).json({msg: 'registering a new account not allowed from the sign-in page'}); + } + /* new user, but check if we already have an account with that email */ + let sql = 'SELECT * from users WHERE email = ?'; + const args = [userProfile.email]; + if (user_sid) { + sql += ' AND user_sid <> ?'; + args.push(user_sid); + } + logger.debug(`sql is ${sql}`); + const [accounts] = await promisePool.execute(sql, args); + if (accounts.length > 0) { + throw new DbErrorBadRequest(`user already exists with email ${userProfile.email}`); + } + } + } + if (userProfile.pristine !== false && !user_sid) { + /* add a new user and account */ + /* get root domain */ + const [sp] = await promisePool.query(queryRootDomainSql, req.body.service_provider_sid); + if (0 === sp.length) throw new Error(`service_provider not found for sid ${req.body.service_provider_sid}`); + if (!sp[0].root_domain) { + throw new Error(`root_domain missing for service provider ${req.body.service_provider_sid}`); + } + + userProfile.root_domain = sp[0].root_domain; + userProfile.account_sid = uuid(); + userProfile.user_sid = uuid(); + + const [r1] = await promisePool.execute(insertAccountSql, + [ + userProfile.account_sid, + req.body.service_provider_sid, + userProfile.name || userProfile.email, + req.body.provider !== 'local', + `wh_secret_${translator.generate()}` + ]); + logger.debug({r1}, 'Result from adding account'); + + /* add to signup history */ + let isReturningUser = false; + try { + await promisePool.execute(insertSignupHistorySql, + [userProfile.email, userProfile.name || userProfile.email]); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + logger.info(`register: user is signing up for a second trial: ${userProfile.email}`); + isReturningUser = true; + } + } + + /* write sample cdrs and alerts in test environment */ + if ('test' === process.env.NODE_ENV) { + await createTestCdrs(writeCdrs, userProfile.account_sid); + await createTestAlerts(writeAlerts, AlertType, userProfile.account_sid); + logger.debug('added test data for cdrs and alerts'); + } + /* assign starter set of products */ + await setupFreeTrial(logger, userProfile.account_sid, isReturningUser); + + /* add a user for the account */ + if (req.body.provider === 'local') { + /* hash password */ + debug(`salting password: ${req.body.password}`); + const passwordHash = await generateHashedPassword(req.body.password); + debug(`hashed password: ${passwordHash}`); + await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid, + userProfile.name, userProfile.email, userProfile.email_activation_code, passwordHash); + debug('added local user'); + } + else { + await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid, + userProfile.name, userProfile.email, userProfile.provider, + userProfile.provider_userid); + } + + Object.assign(userProfile, { + pristine: true, + is_active: req.body.provider !== 'local', + email_validated: userProfile.provider !== 'local', + phone_validated: false, + tutorial_completion: 0, + scope: 'read-write' + }); + } + else if (user_sid) { + /* add a new user for existing account */ + userProfile.user_sid = uuid(); + userProfile.account_sid = account_sid; + + /* changing auth mechanism, add user for existing account */ + logger.debug(`register - creating new user for existing account ${account_sid}`); + if (req.body.provider === 'local') { + /* hash password */ + const passwordHash = await generateHashedPassword(req.body.password); + + await addLocalUser(logger, userProfile.user_sid, userProfile.account_sid, + userProfile.name, userProfile.email, userProfile.email_activation_code, + passwordHash); + + /* note: we deactivate the old user once the new email is validated */ + } + else { + await addOauthUser(logger, userProfile.user_sid, userProfile.account_sid, + userProfile.name, userProfile.email, userProfile.provider, + userProfile.provider_userid); + + /* deactivate the old/replaced user */ + const [r] = await promisePool.execute('DELETE FROM users WHERE user_sid = ?', [user_sid]); + logger.debug({r}, 'register - removed old user'); + } + } + + // generate a json web token for this user + const token = jwt.sign({ + user_sid: userProfile.user_sid, + account_sid: userProfile.account_sid, + email: userProfile.email, + name: userProfile.name + }, process.env.JWT_SECRET, { expiresIn: '1h' }); + + logger.debug({ + user_sid: userProfile.user_sid, + account_sid: userProfile.account_sid + }, 'generated jwt'); + + res.json({jwt: token, ...userProfile}); + + } catch (err) { + debug(err, 'Error'); + sysError(logger, res, err); + } + +}); + + +module.exports = router; diff --git a/lib/routes/api/sbcs.js b/lib/routes/api/sbcs.js index dfc153d..0f6f910 100644 --- a/lib/routes/api/sbcs.js +++ b/lib/routes/api/sbcs.js @@ -1,7 +1,9 @@ const router = require('express').Router(); const Sbc = require('../../models/sbc'); const decorate = require('./decorate'); -const sysError = require('./error'); +const sysError = require('../error'); +//const {DbErrorBadRequest} = require('../../utils/errors'); +//const {promisePool} = require('../../db'); decorate(router, Sbc, ['add', 'delete']); @@ -9,7 +11,16 @@ decorate(router, Sbc, ['add', 'delete']); router.get('/', async(req, res) => { const logger = req.app.locals.logger; try { - const results = await Sbc.retrieveAll(req.query.service_provider_sid); + const service_provider_sid = req.query.service_provider_sid; + /* + if (req.user.hasAccountAuth) { + const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid); + if (0 === r.length) throw new Error('invalid account_sid'); + service_provider_sid = r[0].service_provider_sid; + } + if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query'); + */ + const results = await Sbc.retrieveAll(service_provider_sid); res.status(200).json(results); } catch (err) { sysError(logger, res, err); diff --git a/lib/routes/api/service-providers.js b/lib/routes/api/service-providers.js index 048b148..0a9a582 100644 --- a/lib/routes/api/service-providers.js +++ b/lib/routes/api/service-providers.js @@ -2,7 +2,10 @@ const router = require('express').Router(); const {DbErrorUnprocessableRequest} = require('../../utils/errors'); const Webhook = require('../../models/webhook'); const ServiceProvider = require('../../models/service-provider'); -const sysError = require('./error'); +const Account = require('../../models/account'); +const VoipCarrier = require('../../models/voip-carrier'); +const {hasServiceProviderPermissions, parseServiceProviderSid} = require('./utils'); +const sysError = require('../error'); const decorate = require('./decorate'); const preconditions = { 'delete': noActiveAccounts @@ -16,6 +19,49 @@ async function noActiveAccounts(req, sid) { decorate(router, ServiceProvider, ['delete'], preconditions); +router.use('/:sid/SpeechCredentials', hasServiceProviderPermissions, require('./speech-credentials')); +router.use('/:sid/PredefinedCarriers', hasServiceProviderPermissions, require('./add-from-predefined-carrier')); +router.get('/:sid/Accounts', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = parseServiceProviderSid(req); + const results = await Account.retrieveAll(service_provider_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); +router.get('/:sid/VoipCarriers', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = parseServiceProviderSid(req); + const results = await VoipCarrier.retrieveAllForSP(service_provider_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); +router.post('/:sid/VoipCarriers', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = parseServiceProviderSid(req); + const uuid = await VoipCarrier.make({...req.body, service_provider_sid}); + res.status(201).json({sid: uuid}); + } catch (err) { + sysError(logger, res, err); + } +}); +router.get(':sid/Acccounts', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = parseServiceProviderSid(req); + const results = await Account.retrieveAll(service_provider_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); + /* add */ router.post('/', async(req, res) => { const logger = req.app.locals.logger; diff --git a/lib/routes/api/signin.js b/lib/routes/api/signin.js new file mode 100644 index 0000000..52b1b4b --- /dev/null +++ b/lib/routes/api/signin.js @@ -0,0 +1,85 @@ +const router = require('express').Router(); +//const debug = require('debug')('jambonz:api-server'); +const {DbErrorBadRequest} = require('../../utils/errors'); +const {promisePool} = require('../../db'); +const {verifyPassword} = require('../../utils/password-utils'); +const jwt = require('jsonwebtoken'); +const sysError = require('../error'); + +const validateRequest = async(req) => { + const {email, password} = req.body || {}; + + /* check required properties are there */ + if (!email || !password) throw new DbErrorBadRequest('missing email or password'); +}; + +router.post('/', async(req, res) => { + const {logger, retrieveKey} = req.app.locals; + const {email, password, link} = req.body; + let user; + + try { + if (link) { + const key = `reset-link:${link}`; + const user_sid = await retrieveKey(key); + logger.debug({user_sid}, 'retrieved user from link'); + if (!user_sid) { + return res.sendStatus(403); + } + const [r] = await promisePool.query('SELECT * from users WHERE user_sid = ?', user_sid); + if (0 === r.length) return res.sendStatus(404); + user = r[0]; + } + else { + validateRequest(req); + const [r] = await promisePool.query( + 'SELECT * from users WHERE email = ? AND provider=\'local\' AND email_validated=1', email); + if (0 === r.length) return res.sendStatus(404); + user = r[0]; + + //debug(`password presented is ${password} and hashed_password in db is ${user.hashed_password}`); + const isCorrect = await verifyPassword(user.hashed_password, password); + if (!isCorrect) return res.sendStatus(403); + } + logger.debug({user}, 'signin: retrieved user'); + + const [a] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', user.account_sid); + if (a.length !== 1) throw new Error('database error - account not found for user'); + + const userProfile = Object.assign({}, { + user_sid: user.user_sid, + name: user.name, + email: user.email, + phone: user.phone, + account_sid: user.account_sid, + force_change: !!user.force_change, + provider: user.provider, + provider_userid: user.provider_userid, + scope: user.scope, + phone_validated: !!user.phone_validated, + email_validated: !!user.email_validated + }, { + is_active: !!a[0].is_active, + tutorial_completion: a[0].tutorial_completion, + pristine: false + }); + + // generate a json web token for this session + const token = jwt.sign({ + user_sid: userProfile.user_sid, + account_sid: userProfile.account_sid + }, process.env.JWT_SECRET, { expiresIn: '1h' }); + + logger.debug({ + user_sid: userProfile.user_sid, + account_sid: userProfile.account_sid + }, 'generated jwt'); + + res.json({jwt: token, ...userProfile}); + } catch (err) { + sysError(logger, res, err); + } +}); + + +module.exports = router; diff --git a/lib/routes/api/sip-gateways.js b/lib/routes/api/sip-gateways.js index 3aed34c..b75571f 100644 --- a/lib/routes/api/sip-gateways.js +++ b/lib/routes/api/sip-gateways.js @@ -1,8 +1,53 @@ const router = require('express').Router(); const SipGateway = require('../../models/sip-gateway'); +const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const decorate = require('./decorate'); -const preconditions = {}; +const sysError = require('../error'); -decorate(router, SipGateway, ['*'], preconditions); +const validate = async(req, sid) => { + const {lookupCarrierBySid, lookupSipGatewayBySid} = req.app.locals; + let voip_carrier_sid; + + if (sid) { + const gateway = await lookupSipGatewayBySid(sid); + if (!gateway) throw new DbErrorBadRequest('invalid sip_gateway_sid'); + voip_carrier_sid = gateway.voip_carrier_sid; + } + else { + voip_carrier_sid = req.body.voip_carrier_sid; + if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid'); + } + if (req.hasAccountAuth) { + const carrier = await lookupCarrierBySid(voip_carrier_sid); + if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid'); + if (carrier.account_sid !== req.user.account_sid) { + throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account'); + } + } +}; + +const preconditions = { + 'add': validate, + 'update': validate, + 'delete': validate +}; + +decorate(router, SipGateway, ['add', 'retrieve', 'update', 'delete'], preconditions); + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const voip_carrier_sid = req.query.voip_carrier_sid; + try { + if (!voip_carrier_sid) { + logger.info('GET /SipGateways missing voip_carrier_sid param'); + return res.status(400).json({message: 'missing voip_carrier_sid query param'}); + } + const results = await SipGateway.retrieveForVoipCarrier(voip_carrier_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); module.exports = router; diff --git a/lib/routes/api/sip-realm.js b/lib/routes/api/sip-realm.js new file mode 100644 index 0000000..fb661b7 --- /dev/null +++ b/lib/routes/api/sip-realm.js @@ -0,0 +1,67 @@ +const router = require('express').Router(); +const {promisePool} = require('../../db'); +const {DbErrorBadRequest} = require('../../utils/errors'); +const {createDnsRecords, deleteDnsRecords} = require('../../utils/dns-utils'); +const uuid = require('uuid').v4; +const sysError = require('../error'); +const insertDnsRecords = `INSERT INTO dns_records +(dns_record_sid, account_sid, record_type, record_id) +VALUES `; + + +router.post('/:sip_realm', async(req, res) => { + const logger = req.app.locals.logger; + const account_sid = req.user.account_sid; + const sip_realm = req.params.sip_realm; + try { + const arr = /(.*)\.(.*\..*)$/.exec(sip_realm); + if (!arr) throw new DbErrorBadRequest(`invalid sip_realm: ${sip_realm}`); + const subdomain = arr[1]; + const domain = arr[2]; + + /* update the account */ + const [r] = await promisePool.execute('UPDATE accounts set sip_realm = ? WHERE account_sid = ?', + [sip_realm, account_sid]); + if (r.affectedRows !== 1) throw new Error('failure updating accounts table with sip_realm value'); + + if (process.env.NODE_ENV !== 'test' || process.env.DME_API_KEY) { + /* update DNS provider */ + + /* retrieve sbc addresses */ + const [sbcs] = await promisePool.query('SELECT ipv4 from sbc_addresses'); + if (sbcs.length === 0) throw new Error('no SBC addresses provisioned in the database!'); + const ips = sbcs.map((s) => s.ipv4); + + /* retrieve existing dns records */ + const [old_recs] = await promisePool.query('SELECT record_id from dns_records WHERE account_sid = ?', + account_sid); + + if (old_recs.length > 0) { + /* remove existing records from the database and dns provider */ + await promisePool.query('DELETE from dns_records WHERE account_sid = ?', account_sid); + + const deleted = await deleteDnsRecords(logger, domain, old_recs.map((r) => r.record_id)); + if (!deleted) { + logger.error({old_recs, sip_realm, account_sid}, + 'Failed to remove old dns records when changing sip_realm for account'); + } + } + + /* add the dns records */ + const records = await createDnsRecords(logger, domain, subdomain, ips); + if (!records) throw new Error(`failure updating dns records for ${sip_realm}`); + const values = records.map((r) => { + return `('${uuid()}', '${account_sid}', '${r.type}', ${r.id})`; + }).join(','); + const sql = `${insertDnsRecords}${values};`; + const [result] = await promisePool.execute(sql); + if (result.affectedRows != records.length) throw new Error('failed inserting dns records'); + } + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } +}); + + +module.exports = router; diff --git a/lib/routes/api/smpp-gateways.js b/lib/routes/api/smpp-gateways.js new file mode 100644 index 0000000..a934ef1 --- /dev/null +++ b/lib/routes/api/smpp-gateways.js @@ -0,0 +1,53 @@ +const router = require('express').Router(); +const SmppGateway = require('../../models/smpp_gateway'); +const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); +const decorate = require('./decorate'); +const sysError = require('../error'); + +const validate = async(req, sid) => { + const {lookupCarrierBySid, lookupSmppGatewayBySid} = req.app.locals; + let voip_carrier_sid; + + if (sid) { + const gateway = await lookupSmppGatewayBySid(sid); + if (!gateway) throw new DbErrorBadRequest('invalid smpp_gateway_sid'); + voip_carrier_sid = gateway.voip_carrier_sid; + } + else { + voip_carrier_sid = req.body.voip_carrier_sid; + if (!voip_carrier_sid) throw new DbErrorBadRequest('missing voip_carrier_sid'); + } + if (req.hasAccountAuth) { + const carrier = await lookupCarrierBySid(voip_carrier_sid); + if (!carrier) throw new DbErrorBadRequest('invalid voip_carrier_sid'); + if (carrier.account_sid !== req.user.account_sid) { + throw new DbErrorUnprocessableRequest('user can not add gateway for voip_carrier belonging to other account'); + } + } +}; + +const preconditions = { + 'add': validate, + 'update': validate, + 'delete': validate +}; + +decorate(router, SmppGateway, ['add', 'retrieve', 'update', 'delete'], preconditions); + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const voip_carrier_sid = req.query.voip_carrier_sid; + try { + if (!voip_carrier_sid) { + logger.info('GET /SmppGateways missing voip_carrier_sid param'); + return res.status(400).json({message: 'missing voip_carrier_sid query param'}); + } + const results = await SmppGateway.retrieveForVoipCarrier(voip_carrier_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/smpps.js b/lib/routes/api/smpps.js new file mode 100644 index 0000000..716e48e --- /dev/null +++ b/lib/routes/api/smpps.js @@ -0,0 +1,30 @@ +const router = require('express').Router(); +const Smpp = require('../../models/smpp'); +const decorate = require('./decorate'); +const sysError = require('../error'); +//const {DbErrorBadRequest} = require('../../utils/errors'); +//const {promisePool} = require('../../db'); + +decorate(router, Smpp, ['add', 'delete']); + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + try { + const service_provider_sid = req.query.service_provider_sid; + /* + if (req.user.hasAccountAuth) { + const [r] = await promisePool.query('SELECT * from accounts WHERE account_sid = ?', req.user.account_sid); + if (0 === r.length) throw new Error('invalid account_sid'); + service_provider_sid = r[0].service_provider_sid; + } + if (!service_provider_sid) throw new DbErrorBadRequest('missing service_provider_sid in query'); + */ + const results = await Smpp.retrieveAll(service_provider_sid); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/sms-inbound.js b/lib/routes/api/sms-inbound.js index eaf09d8..f9f0487 100644 --- a/lib/routes/api/sms-inbound.js +++ b/lib/routes/api/sms-inbound.js @@ -2,7 +2,7 @@ const router = require('express').Router(); const request = require('request'); const getProvider = require('../../utils/sms-provider'); const uuidv4 = require('uuid/v4'); -const sysError = require('./error'); +const sysError = require('../error'); let idx = 0; diff --git a/lib/routes/api/sms-outbound.js b/lib/routes/api/sms-outbound.js index 277e190..ddbc2ed 100644 --- a/lib/routes/api/sms-outbound.js +++ b/lib/routes/api/sms-outbound.js @@ -1,6 +1,6 @@ const router = require('express').Router(); const getProvider = require('../../utils/sms-provider'); -const sysError = require('./error'); +const sysError = require('../error'); router.post('/', async(req, res) => { const { logger } = req.app.locals; diff --git a/lib/routes/api/speech-credentials.js b/lib/routes/api/speech-credentials.js new file mode 100644 index 0000000..8567794 --- /dev/null +++ b/lib/routes/api/speech-credentials.js @@ -0,0 +1,248 @@ +const router = require('express').Router(); +const SpeechCredential = require('../../models/speech-credential'); +const sysError = require('../error'); +const {decrypt, encrypt} = require('../../utils/encrypt-decrypt'); +const {parseAccountSid, parseServiceProviderSid} = require('./utils'); +const {DbErrorUnprocessableRequest, DbErrorBadRequest} = require('../../utils/errors'); +const { + testGoogleTts, + testGoogleStt, + testAwsTts, + testAwsStt +} = require('../../utils/speech-utils'); + +router.post('/', async(req, res) => { + const logger = req.app.locals.logger; + const {use_for_stt, use_for_tts, vendor, service_key, access_key_id, secret_access_key, aws_region} = req.body; + const {account_sid} = req.user; + let service_provider_sid; + if (!account_sid) { + if (!req.user.hasServiceProviderAuth) { + logger.error('POST /SpeechCredentials invalid credentials'); + return res.send(403); + } + service_provider_sid = parseServiceProviderSid(req); + } + try { + let encrypted_credential; + if (vendor === 'google') { + let obj; + if (!service_key) throw new DbErrorBadRequest('invalid json key: service_key is required'); + try { + obj = JSON.parse(service_key); + if (!obj.client_email || !obj.private_key) { + throw new DbErrorBadRequest('invalid google service account key'); + } + } + catch (err) { + throw new DbErrorBadRequest('invalid google service account key - not JSON'); + } + encrypted_credential = encrypt(service_key); + } + else if (vendor === 'aws') { + const data = JSON.stringify({ + aws_region: aws_region || 'us-east-1', + access_key_id, + secret_access_key + }); + encrypted_credential = encrypt(data); + } + else throw new DbErrorBadRequest(`invalid speech vendor ${vendor}`); + const uuid = await SpeechCredential.make({ + account_sid, + service_provider_sid, + vendor, + use_for_tts, + use_for_stt, + credential: encrypted_credential + }); + res.status(201).json({sid: uuid}); + } catch (err) { + sysError(logger, res, err); + } +}); + +/** + * retrieve all speech credentials for an account + */ +router.get('/', async(req, res) => { + let service_provider_sid; + const account_sid = parseAccountSid(req); + if (!account_sid) service_provider_sid = parseServiceProviderSid(req); + const logger = req.app.locals.logger; + try { + const creds = account_sid ? + await SpeechCredential.retrieveAll(account_sid) : + await SpeechCredential.retrieveAllForSP(service_provider_sid); + + res.status(200).json(creds.map((c) => { + const {credential, ...obj} = c; + if ('google' === obj.vendor) { + obj.service_key = decrypt(credential); + } + else if ('aws' === obj.vendor) { + const o = decrypt(credential); + obj.access_key_id = o.access_key_id; + obj.secret_access_key = o.secret_access_key; + } + return obj; + })); + } catch (err) { + sysError(logger, res, err); + } +}); + +/** + * retrieve a specific speech credential + */ +router.get('/:sid', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + try { + const cred = await SpeechCredential.retrieve(sid); + if (0 === cred.length) return res.sendStatus(404); + const {credential, ...obj} = cred[0]; + if ('google' === obj.vendor) { + obj.service_key = decrypt(credential); + } + else if ('aws' === obj.vendor) { + const o = JSON.parse(decrypt(credential)); + obj.access_key_id = o.access_key_id; + obj.secret_access_key = o.secret_access_key; + } + res.status(200).json(obj); + } catch (err) { + sysError(logger, res, err); + } +}); + +/** + * delete a speech credential + */ +router.delete('/:sid', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + try { + const count = await SpeechCredential.remove(sid); + if (0 === count) return res.sendStatus(404); + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } +}); + + +/** + * update a speech credential -- we only allow use_for_tts and use_for_stt to be updated + */ +router.put('/:sid', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + try { + const {use_for_tts, use_for_stt} = req.body; + if (typeof use_for_tts === 'undefined' && typeof use_for_stt === 'undefined') { + throw new DbErrorUnprocessableRequest('use_for_tts and use_for_stt are the only updateable fields'); + } + const obj = {}; + if (typeof use_for_tts !== 'undefined') { + obj.use_for_tts = use_for_tts; + } + if (typeof use_for_stt !== 'undefined') { + obj.use_for_stt = use_for_stt; + } + + const rowsAffected = await SpeechCredential.update(sid, obj); + if (rowsAffected === 0) { + return res.sendStatus(404); + } + res.status(204).end(); + } catch (err) { + sysError(logger, res, err); + } +}); + + +/** + * Test a credential + */ +router.get('/:sid/test', async(req, res) => { + const sid = req.params.sid; + const logger = req.app.locals.logger; + try { + const creds = await SpeechCredential.retrieve(sid); + if (!creds || 0 === creds.length) return res.sendStatus(404); + + const cred = creds[0]; + const credential = JSON.parse(decrypt(cred.credential)); + const results = { + tts: { + status: 'not tested' + }, + stt: { + status: 'not tested' + } + }; + if (cred.vendor === 'google') { + if (!credential.client_email || !credential.private_key) { + throw new DbErrorUnprocessableRequest('uploaded file is not a google service key'); + } + + if (cred.use_for_tts) { + try { + await testGoogleTts(logger, credential); + results.tts.status = 'ok'; + SpeechCredential.ttsTestResult(sid, true); + } catch (err) { + results.tts = {status: 'fail', reason: err.message}; + SpeechCredential.ttsTestResult(sid, false); + } + } + + if (cred.use_for_stt) { + try { + await testGoogleStt(logger, credential); + results.stt.status = 'ok'; + SpeechCredential.sttTestResult(sid, true); + } catch (err) { + results.stt = {status: 'fail', reason: err.message}; + SpeechCredential.sttTestResult(sid, false); + } + } + } + else if (cred.vendor === 'aws') { + if (cred.use_for_tts) { + try { + await testAwsTts(logger, { + accessKeyId: credential.access_key_id, + secretAccessKey: credential.secret_access_key, + region: credential.aws_region || process.env.AWS_REGION + }); + results.tts.status = 'ok'; + SpeechCredential.ttsTestResult(sid, true); + } catch (err) { + results.tts = {status: 'fail', reason: err.message}; + SpeechCredential.ttsTestResult(sid, false); + } + } + if (cred.use_for_stt) { + try { + await testAwsStt(logger, { + accessKeyId: credential.access_key_id, + secretAccessKey: credential.secret_access_key, + region: credential.aws_region || process.env.AWS_REGION + }); + results.stt.status = 'ok'; + SpeechCredential.sttTestResult(sid, true); + } catch (err) { + results.stt = {status: 'fail', reason: err.message}; + SpeechCredential.sttTestResult(sid, false); + } + } + } + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/stripe-customer-id.js b/lib/routes/api/stripe-customer-id.js new file mode 100644 index 0000000..04f54fe --- /dev/null +++ b/lib/routes/api/stripe-customer-id.js @@ -0,0 +1,48 @@ +const router = require('express').Router(); +const Account = require('../../models/account'); +const sysError = require('../error'); + +/* list */ +router.get('/', async(req, res) => { + const {createCustomer} = require('../../utils/stripe-utils'); + const logger = req.app.locals.logger; + try { + const {account_sid, email, name} = req.user; + logger.debug({account_sid, email, name}, 'GET /StripeCustomerId'); + const results = await Account.retrieve(account_sid); + if (results.length === 0) return res.sendStatus(404); + const account = results[0]; + + /* is account already provisioned in Stripe ? */ + if (account.stripe_customer_id) return res.status(200).json({stripe_customer_id: account.stripe_customer_id}); + + /* no - provision it now */ + const customer = await createCustomer(logger, account_sid, email, name); + account.stripe_customer_id = customer.id; + await Account.updateStripeCustomerId(account_sid, customer.id); + res.status(200).json({stripe_customer_id: customer.id}); + } catch (err) { + sysError(logger, res, err); + } +}); + +/* delete */ +router.delete('/', async(req, res) => { + const {deleteCustomer} = require('../../utils/stripe-utils'); + const logger = req.app.locals.logger; + const {account_sid} = req.user; + try { + const acc = await Account.retrieve(account_sid); + logger.debug({acc}, 'retrieved account'); + if (!acc || 0 === acc.length || !acc[0].stripe_customer_id) return res.sendStatus(404); + const {stripe_customer_id} = acc[0]; + logger.info(`deleting stripe customer id ${stripe_customer_id}`); + await deleteCustomer(logger, stripe_customer_id); + await Account.updateStripeCustomerId(account_sid, null); + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/subscriptions.js b/lib/routes/api/subscriptions.js new file mode 100644 index 0000000..f9b2d68 --- /dev/null +++ b/lib/routes/api/subscriptions.js @@ -0,0 +1,334 @@ +const router = require('express').Router(); +const {DbErrorBadRequest} = require('../../utils/errors'); +const Account = require('../../models/account'); +const { + createCustomer, + retrieveCustomer, + updateCustomer, + createSubscription, + retrieveSubscription, + updateSubscription, + retrieveInvoice, + payOutstandingInvoicesForCustomer, + attachPaymentMethod, + detachPaymentMethod, + retrievePaymentMethod, + retrieveUpcomingInvoice +} = require('../../utils/stripe-utils'); +const {setupFreeTrial} = require('./utils'); +const sysError = require('../error'); +const actions = [ + 'upgrade-to-paid', + 'downgrade-to-free', + 'update-payment-method', + 'update-quantities' +]; + +const handleError = async(logger, method, res, err) => { + if ('StatusError' === err.name) { + const text = await err.text(); + let details; + if (text) { + details = JSON.parse(text); + logger.info({details}, `${method} failed`); + } + if (402 === err.statusCode && details) { + return res.status(err.statusCode).json(details); + } + return res.sendStatus(err.statusCode); + } + sysError(logger, res, err); +}; + +/** + * We handle 3 possible outcomes + * - the initial payment was successful + * - there was a card error on the initial payment (i.e. decline) + * - there is a requirement for additional authentication (e.g. SCA) + * see: https://stripe.com/docs/billing/migration/strong-customer-authentication + * @param {*} req + * @param {*} res + * @param {*} subscription + */ +const handleSubscriptionOutcome = async(req, res, subscription) => { + const logger = req.app.locals.logger; + const {account_sid} = subscription.metadata; + const {status, latest_invoice} = subscription; + const {payment_intent} = latest_invoice; + + /* success case */ + if ('active' == status && 'paid' === latest_invoice.status && 'succeeded' === payment_intent.status) { + await Account.activateSubscription(logger, account_sid, subscription.id, 'upgrade to paid plan'); + return res.status(201).json({ + status: 'success', + chargedAmount: latest_invoice.amount_paid, + currency: payment_intent.currency, + statementDescriptor: payment_intent.statement_descriptor + }); + } + + /* card error */ + if ('incomplete' == status && 'open' === latest_invoice.status && + 'requires_payment_method' === payment_intent.status) { + return res.status(201).json({ + status: 'card error', + subscription: subscription.id, + client_secret: payment_intent.client_secret, + reason: payment_intent.last_payment_error.message + }); + } + + /* more authentication required */ + if ('incomplete' == status && 'open' === latest_invoice.status && 'requires_action' === payment_intent.status) { + return res.status(201).json({ + status: 'action required', + subscription: subscription.id, + client_secret: payment_intent.client_secret + }); + } + + throw new Error( + `handleSubscriptionOutcome unexpected status ${status}:${latest_invoice.status}:${payment_intent.status}`); +}; + +/** + * Transition from free --> paid + * Create customer in Stripe, if needed + * Set the default payment method + * Create a subscription + * @param {*} req + * @param {*} res + */ +const upgradeToPaidPlan = async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid, name, email} = req.user; + const {payment_method_id, products} = req.body; + const arr = await Account.retrieve(req.user.account_sid); + const account = arr[0]; + + /* retrieve stripe customer id locally, provision on Stripe if needed */ + logger.debug({account}, 'upgradeToPaidPlan retrieved account'); + let stripe_customer_id = account.stripe_customer_id; + if (!stripe_customer_id) { + logger.debug('upgradeToPaidPlan provisioning customer'); + const customer = await createCustomer(logger, account_sid, email, name); + logger.debug(`upgradeToPaidPlan provisioned customer_id ${customer.id}`); + await Account.updateStripeCustomerId(account_sid, customer.id); + stripe_customer_id = customer.id; + } + + /* attach the payment method to the customer and make it their default */ + const pm = await attachPaymentMethod(logger, payment_method_id, stripe_customer_id); + const customer = await updateCustomer(logger, stripe_customer_id, { + invoice_settings: { + default_payment_method: req.body.payment_method_id, + } + }); + logger.debug({customer}, 'successfully updated customer'); + + /* create a pending subscription -- will be activated on invoice.paid */ + const account_subscription_sid = await Account.provisionPendingSubscription(logger, account_sid, products, pm); + + /* create the subscription in Stripe */ + const items = products.map((product) => { + return { + price: product.price_id, + quantity: product.quantity, + metadata: { + product_sid: product.product_sid + } + }; + }); + logger.debug({items}, 'creating subscription'); + const subscription = await createSubscription(logger, stripe_customer_id, + {account_sid, account_subscription_sid}, items); + logger.debug({subscription}, 'created subscription'); + + await handleSubscriptionOutcome(req, res, subscription); +}; +const downgradeToFreePlan = async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid} = req.user; + try { + await setupFreeTrial(logger, account_sid); + return res.status(200).json({status: 'success'}); + } catch (err) { + handleError(logger, 'downgradeToFreePlan', res, err); + } +}; +const updatePaymentMethod = async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid} = req.user; + try { + const {payment_method_id} = req.body; + const arr = await Account.retrieve(req.user.account_sid); + const account = arr[0]; + if (!account.stripe_customer_id) { + throw new DbErrorBadRequest(`Account ${account_sid} is not provisioned in Stripe`); + } + const customer = await retrieveCustomer(logger, account.stripe_customer_id); + //logger.debug({customer}, 'retrieved customer'); + + /* attach the payment method to the customer */ + const pm = await attachPaymentMethod(logger, payment_method_id, account.stripe_customer_id); + logger.debug({pm}, 'attached payment method to customer'); + + /* update last4 etc in our db */ + await Account.updatePaymentInfo(logger, account_sid, pm); + + /* make it the customer's default payment method */ + await updateCustomer(logger, account.stripe_customer_id, { + invoice_settings: { + default_payment_method: req.body.payment_method_id, + } + }); + + /* detach the customer's old payment method */ + const old_pm = customer.default_source || customer.invoice_settings.default_payment_method; + if (old_pm) await detachPaymentMethod(logger, old_pm); + + /* if the customer has an unpaid invoice, try to pay it */ + const success = await payOutstandingInvoicesForCustomer(logger, account.stripe_customer_id); + res.status(200).json({ + status: success ? 'success' : 'failed to pay outstanding invoices' + }); + } catch (err) { + handleError(logger, 'updatePaymentMethod', res, err); + } +}; +const updateQuantities = async(req, res) => { + /** + * see https://stripe.com/docs/billing/subscriptions/upgrade-downgrade#immediate-payment + * and https://stripe.com/docs/billing/subscriptions/pending-updates + */ + const logger = req.app.locals.logger; + const {account_sid} = req.user; + const {products, dry_run} = req.body; + + if (!products || !Array.isArray(products) || + 0 === products.length || + products.find((p) => !p.price_id || !p.product_sid)) { + logger.info({products}, 'Subscription:updateQuantities invalid products'); + return res.sendStatus(400); + } + + try { + const account_subscription = await Account.getSubscription(req.user.account_sid); + if (!account_subscription || !account_subscription.stripe_subscription_id) { + logger.info(`Subscription:updateQuantities No active subscription found for account_sid ${account_sid}`); + return res.sendStatus(400); + } + const subscription_id = account_subscription.stripe_subscription_id; + + const subscription = await retrieveSubscription(logger, subscription_id); + logger.debug({subscription}, 'retrieved existing subscription'); + const pm = await retrievePaymentMethod(logger, account_subscription.stripe_payment_method_id); + logger.debug({pm}, 'retrieved existing payment method'); + const items = products.map((product) => { + const existingItem = subscription.items.data.find((i) => i.price.id === product.price_id); + const obj = { + quantity: product.quantity, + }; + return Object.assign(obj, existingItem ? {id: existingItem.id} : {price_id: product.price_id}); + }); + + if (dry_run) { + const invoice = await retrieveUpcomingInvoice(logger, subscription.customer, subscription.id, items); + logger.debug({invoice}, 'dry run - upcoming invoice'); + const dt = new Date(invoice.next_payment_attempt * 1000); + const sum = (acc, current) => acc + current.amount; + const prorated_cost = invoice.lines.data + .filter((l) => l.proration === true) + .reduce(sum, 0); + const monthly_cost = invoice.lines.data + .filter((l) => l.proration === false) + .reduce(sum, 0); + return res.status(201).json({ + currency: invoice.currency, + prorated_cost, + monthly_cost, + next_invoice_date: dt.toDateString() + }); + } + + /* create a pending subscription */ + await Account.provisionPendingSubscription(logger, account_sid, products, + pm, subscription_id); + + /* update the subscription in Stripe */ + const updated = await updateSubscription(logger, subscription_id, items); + logger.debug({updated}, 'updated subscription'); + + /* get latest invoice, to see if payment is needed */ + const invoice = await retrieveInvoice(logger, updated.latest_invoice); + logger.debug({invoice}, 'latest invoice'); + + if ('paid' === invoice.status) { + logger.debug('activating pending subscription to new quantities since no invoice outstanding'); + await Account.activateSubscription(logger, account_sid, subscription_id, 'selected new capacities'); + return res.status(201).json({ + status: 'success' + }); + } + else { + return res.status(201).json({ + status: 'failed', + reason: 'payment required' + }); + } + } catch (err) { + handleError(logger, 'updatePaymentMethod', res, err); + } +}; + +/* create */ +router.post('/', async(req, res) => { + const logger = req.app.locals.logger; + try { + const {action, payment_method_id, products} = req.body; + + if (!actions.includes(action)) throw new DbErrorBadRequest('invalid or missing action'); + if ('update-payment-method' === action && typeof payment_method_id !== 'string') { + throw new DbErrorBadRequest('missing payment_method_id'); + } + if ('upgrade-to-paid' === action && (!Array.isArray(products) || 0 === products.length)) { + throw new DbErrorBadRequest('missing products'); + } + if ('update-quantities' === action && (!Array.isArray(products) || 0 === products.length)) { + throw new DbErrorBadRequest('missing products'); + } + + switch (action) { + case 'upgrade-to-paid': + await upgradeToPaidPlan(req, res); + break; + case 'downgrade-to-free': + await downgradeToFreePlan(req, res); + break; + case 'update-payment-method': + await updatePaymentMethod(req, res); + break; + case 'update-quantities': + await updateQuantities(req, res); + break; + } + } catch (err) { + handleError(logger, 'POST /Subscription', res, err); + } +}); + +/* get */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + const {account_sid} = req.user; + try { + const subscription = await Account.getSubscription(account_sid); + if (!subscription || !subscription.stripe_subscription_id) return res.sendStatus(404); + const sub = await retrieveSubscription(logger, subscription.stripe_subscription_id); + res.status(200).json(sub); + } catch (err) { + handleError(logger, 'GET /Subscription', res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/api/users.js b/lib/routes/api/users.js index 44d2de7..f81f716 100644 --- a/lib/routes/api/users.js +++ b/lib/routes/api/users.js @@ -1,95 +1,179 @@ +//const assert = require('assert'); +//const debug = require('debug')('jambonz:api-server'); const router = require('express').Router(); -const crypto = require('crypto'); -const {getMysqlConnection} = require('../../db'); - +const {DbErrorBadRequest} = require('../../utils/errors'); +const {generateHashedPassword, verifyPassword} = require('../../utils/password-utils'); +const {promisePool} = require('../../db'); +const {decrypt} = require('../../utils/encrypt-decrypt'); +const sysError = require('../error'); +const retrieveMyDetails = `SELECT * +FROM users user +JOIN accounts AS account ON account.account_sid = user.account_sid +LEFT JOIN service_providers as sp ON account.service_provider_sid = sp.service_provider_sid +WHERE user.user_sid = ?`; const retrieveSql = 'SELECT * from users where user_sid = ?'; -const updateSql = 'UPDATE users set hashed_password = ?, salt = ?, force_change = false WHERE user_sid = ?'; -const tokenSql = 'SELECT token from api_keys where account_sid IS NULL AND service_provider_sid IS NULL'; +const retrieveProducts = `SELECT * +FROM account_products +JOIN products ON account_products.product_sid = products.product_sid +JOIN account_subscriptions ON account_products.account_subscription_sid = account_subscriptions.account_subscription_sid +WHERE account_subscriptions.account_sid = ? +AND account_subscriptions.effective_end_date IS NULL +AND account_subscriptions.pending=0`; +const updateSql = 'UPDATE users set hashed_password = ?, force_change = false WHERE user_sid = ?'; +const retrieveStaticIps = 'SELECT * FROM account_static_ips WHERE account_sid = ?'; -const genRandomString = (len) => { - return crypto.randomBytes(Math.ceil(len / 2)) - .toString('hex') /** convert to hexadecimal format */ - .slice(0, len); /** return required number of characters */ -}; +const validateRequest = async(user_sid, payload) => { + const {old_password, new_password, name, email, email_activation_code} = payload; -const sha512 = function(password, salt) { - const hash = crypto.createHmac('sha512', salt); /** Hashing algorithm sha512 */ - hash.update(password); - var value = hash.digest('hex'); - return { - salt:salt, - passwordHash:value - }; -}; + const [r] = await promisePool.query(retrieveSql, user_sid); + if (r.length === 0) return null; + const user = r[0]; -const saltHashPassword = (userpassword) => { - var salt = genRandomString(16); /** Gives us salt of length 16 */ - return sha512(userpassword, salt); -}; - -router.put('/:user_sid', (req, res) => { - const logger = req.app.locals.logger; - const {old_password, new_password} = req.body; - if (!old_password || !new_password) { - logger.info('Bad PUT to /Users is missing old_password or new password'); - return res.sendStatus(400); + if ((old_password && !new_password) || (new_password && !old_password)) { + throw new DbErrorBadRequest('new_password and old_password both required'); + } + if (new_password && name) throw new DbErrorBadRequest('can not change name and password simultaneously'); + if (new_password && user.provider !== 'local') { + throw new DbErrorBadRequest('can not change password when using oauth2'); } - getMysqlConnection((err, conn) => { - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - conn.query(retrieveSql, [req.params.user_sid], (err, results) => { - conn.release(); - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - if (0 === results.length) { - logger.info(`Failed to find user with sid ${req.params.user_sid}`); - return res.sendStatus(404); - } + if ((email && !email_activation_code) || (email_activation_code && !email)) { + throw new DbErrorBadRequest('email and email_activation_code both required'); + } + if (!name && !new_password && !email) throw new DbErrorBadRequest('no updates requested'); - logger.info({results}, 'successfully retrieved user'); - const old_salt = results[0].salt; - const old_hashed_password = results[0].hashed_password; + return user; +}; - const {passwordHash} = sha512(old_password, old_salt); - if (old_hashed_password !== passwordHash) return res.sendStatus(403); +router.get('/me', async(req, res) => { + const logger = req.app.locals.logger; + const {user_sid} = req.user; - getMysqlConnection((err, conn) => { - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - const {salt, passwordHash} = saltHashPassword(new_password); - conn.query(updateSql, [passwordHash, salt, req.params.user_sid], (err, r) => { - conn.release(); - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - if (0 === r.changedRows) { - logger.error('Failed updating database with new password'); - return res.sendStatus(500); - } - conn.query(tokenSql, (err, tokenResults) => { - conn.release(); - if (err) { - logger.error({err}, 'Error getting db connection'); - return res.sendStatus(500); - } - if (0 === tokenResults.length) { - logger.error('Database has no admin token provisioned...run reset_admin_password'); - return res.sendStatus(500); - } - res.json({user_sid: results[0].user_sid, token: tokenResults[0].token}); - }); - }); - }); + if (!user_sid) return res.sendStatus(403); + + try { + const [r] = await promisePool.query({sql: retrieveMyDetails, nestTables: true}, user_sid); + logger.debug(r, 'retrieved user details'); + const payload = r[0]; + const {user, account, sp} = payload; + ['hashed_password', 'salt', 'phone_activation_code', 'email_activation_code', 'account_sid'].forEach((prop) => { + delete user[prop]; }); - }); + ['email_validated', 'phone_validated', 'force_change'].forEach((prop) => user[prop] = !!user[prop]); + ['is_active'].forEach((prop) => account[prop] = !!account[prop]); + account.root_domain = sp.root_domain; + delete payload.sp; + + /* get api keys */ + const [keys] = await promisePool.query('SELECT * from api_keys WHERE account_sid = ?', account.account_sid); + payload.api_keys = keys.map((k) => { + return { + api_key_sid: k.api_key_sid, + //token: k.token.replace(/.(?=.{4,}$)/g, '*'), + token: k.token, + last_used: k.last_used, + created_at: k.created_at + }; + }); + + /* get products */ + const [products] = await promisePool.query({sql: retrieveProducts, nestTables: true}, account.account_sid); + if (!products.length || !products[0].account_subscriptions) { + throw new Error('account is missing a subscription'); + } + const account_subscription = products[0].account_subscriptions; + payload.subscription = { + status: 'active', + account_subscription_sid: account_subscription.account_subscription_sid, + start_date: account_subscription.effective_start_date, + products: products.map((prd) => { + return { + name: prd.products.name, + units: prd.products.unit_label, + quantity: prd.account_products.quantity + }; + }) + }; + if (account_subscription.pending) { + Object.assign(payload.subscription, { + status: 'suspended', + suspend_reason: account_subscription.pending_reason + }); + } + const { + last4, + exp_month, + exp_year, + card_type, + stripe_statement_descriptor + } = account_subscription; + if (last4) { + const real_last4 = decrypt(last4); + Object.assign(payload.subscription, { + last4: real_last4, + exp_month, + exp_year, + card_type, + statement_descriptor: stripe_statement_descriptor + }); + } + + /* get static ips */ + const [static_ips] = await promisePool.query(retrieveStaticIps, account.account_sid); + payload.static_ips = static_ips.map((r) => r.public_ipv4); + + logger.debug({payload}, 'returning user details'); + + res.json(payload); + } catch (err) { + sysError(logger, res, err); + } +}); + +router.put('/:user_sid', async(req, res) => { + const logger = req.app.locals.logger; + const {user_sid} = req.params; + const {old_password, new_password, name, email, email_activation_code} = req.body; + + if (req.user.user_sid && req.user.user_sid !== user_sid) return res.sendStatus(403); + + try { + const user = await validateRequest(user_sid, req.body); + if (!user) return res.sendStatus(404); + + if (new_password) { + const old_hashed_password = user.hashed_password; + + const isCorrect = await verifyPassword(old_hashed_password, old_password); + if (!isCorrect) { + //debug(`PUT /Users/:sid pwd ${old_password} does not match hash ${old_hashed_password}`); + return res.sendStatus(403); + } + const passwordHash = await generateHashedPassword(new_password); + //debug(`updating hashed_password to ${passwordHash}`); + const r = await promisePool.execute(updateSql, [passwordHash, user_sid]); + if (0 === r.changedRows) throw new Error('database update failed'); + } + + if (name) { + const r = await promisePool.execute('UPDATE users SET name = ? WHERE user_sid = ?', [name, user_sid]); + if (0 === r.changedRows) throw new Error('database update failed'); + } + + if (email) { + const r = await promisePool.execute( + 'UPDATE users SET email = ?, email_activation_code = ?, email_validated = 0 WHERE user_sid = ?', + [email, email_activation_code, user_sid]); + if (0 === r.changedRows) throw new Error('database update failed'); + + if (process.env.NODE_ENV !== 'test') { + //TODO: send email with activation code + } + } + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } }); diff --git a/lib/routes/api/utils.js b/lib/routes/api/utils.js new file mode 100644 index 0000000..8aa9dcb --- /dev/null +++ b/lib/routes/api/utils.js @@ -0,0 +1,178 @@ +const uuid = require('uuid').v4; +const Account = require('../../models/account'); +const {promisePool} = require('../../db'); +const {cancelSubscription, detachPaymentMethod} = require('../../utils/stripe-utils'); +const freePlans = require('../../utils/free_plans'); +const insertAccountSubscriptionSql = `INSERT INTO account_subscriptions +(account_subscription_sid, account_sid) +values (?, ?)`; +const replaceOldSubscriptionSql = `UPDATE account_subscriptions +SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ? +WHERE account_subscription_sid = ?`; + +const setupFreeTrial = async(logger, account_sid, isReturningUser) => { + const sid = uuid(); + + /* see if we have an existing subscription */ + const account_subscription = await Account.getSubscription(account_sid); + const planType = account_subscription || isReturningUser ? 'free' : 'trial'; + logger.debug({account_subscription}, `setupFreeTrial: assigning ${account_sid} to ${planType} plan`); + + /* create a subscription */ + await promisePool.execute(insertAccountSubscriptionSql, [sid, account_sid]); + + /* add products to it */ + const [products] = await promisePool.query('SELECT * from products'); + const name2Product = new Map(); + products.forEach((p) => name2Product.set(p.category, p.product_sid)); + + await Promise.all(freePlans[planType].map((p) => { + const data = { + account_product_sid: uuid(), + account_subscription_sid: sid, + product_sid: name2Product.get(p.category), + quantity: p.quantity + }; + return promisePool.query('INSERT INTO account_products SET ?', data); + })); + logger.debug({products}, 'setupFreeTrial: added products'); + + /* disable the old subscription, if any */ + if (account_subscription) { + const { + account_subscription_sid, + stripe_subscription_id, + stripe_payment_method_id + } = account_subscription; + await promisePool.execute(replaceOldSubscriptionSql, [ + 'downgraded to free plan', account_subscription_sid]); + logger.debug('setupFreeTrial: deactivated previous plan'); + + const promises = []; + if (stripe_subscription_id) { + logger.debug(`setupFreeTrial: deactivating subscription ${stripe_subscription_id}`); + promises.push(cancelSubscription(logger, stripe_subscription_id)); + } + if (stripe_payment_method_id) { + promises.push(detachPaymentMethod(logger, stripe_payment_method_id)); + } + if (promises.length) await Promise.all(promises); + } + + /* update account.plan */ + await promisePool.execute( + 'UPDATE accounts SET plan_type = ? WHERE account_sid = ?', + [planType, account_sid]); +}; + +const createTestCdrs = async(writeCdrs, account_sid) => { + const points = 2000; + const data = []; + const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)); + const now = new Date(); + const increment = (now.getTime() - start.getTime()) / points; + for (let i = 0 ; i < points; i++) { + const attempted_at = new Date(start.getTime() + (i * increment)); + const failed = 0 === i % 5; + data.push({ + call_sid: 'b6f48929-8e86-4d62-ae3b-64fb574d91f6', + from: '15083084809', + to: '18882349999', + answered: !failed, + sip_callid: '685cd008-0a66-4974-b37a-bdd6d9a3c4aa@192.168.1.100', + sip_status: 200, + duration: failed ? 0 : 45, + attempted_at: attempted_at.getTime(), + answered_at: attempted_at.getTime() + 3000, + terminated_at: attempted_at.getTime() + 45000, + termination_reason: 'caller hungup', + host: '192.168.1.100', + remote_host: '3.55.24.34', + account_sid, + direction: 0 === i % 2 ? 'inbound' : 'outbound', + trunk: 0 === i % 2 ? 'twilio' : 'user' + }); + } + + await writeCdrs(data); + +}; + +const createTestAlerts = async(writeAlerts, AlertType, account_sid) => { + const points = 100; + const data = []; + const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)); + const now = new Date(); + const increment = (now.getTime() - start.getTime()) / points; + for (let i = 0 ; i < points; i++) { + const timestamp = new Date(start.getTime() + (i * increment)); + const scenario = i % 5; + switch (scenario) { + case 0: + data.push({timestamp, account_sid, + alert_type: AlertType.WEBHOOK_STATUS_FAILURE, url: 'http://foo.bar', status: 404}); + break; + case 1: + data.push({timestamp, account_sid, alert_type: AlertType.WEBHOOK_CONNECTION_FAILURE, url: 'http://foo.bar'}); + break; + case 2: + data.push({timestamp, account_sid, alert_type: AlertType.TTS_NOT_PROVISIONED, vendor: 'google'}); + break; + case 3: + data.push({timestamp, account_sid, alert_type: AlertType.CARRIER_NOT_PROVISIONED}); + break; + case 4: + data.push({timestamp, account_sid, alert_type: AlertType.CALL_LIMIT, count: 50}); + break; + default: + break; + } + } + + await writeAlerts(data); + +}; + +const parseServiceProviderSid = (req) => { + const arr = /ServiceProviders\/([^\/]*)/.exec(req.originalUrl); + if (arr) return arr[1]; +}; + +const parseAccountSid = (req) => { + const arr = /Accounts\/([^\/]*)/.exec(req.originalUrl); + if (arr) return arr[1]; +}; + +const hasAccountPermissions = (req, res, next) => { + if (req.user.hasScope('admin')) return next(); + if (req.user.hasScope('account')) { + const account_sid = parseAccountSid(req); + if (account_sid === req.user.account_sid) return next(); + } + res.status(403).json({ + status: 'fail', + message: 'insufficient privileges' + }); +}; + +const hasServiceProviderPermissions = (req, res, next) => { + if (req.user.hasScope('admin')) return next(); + if (req.user.hasScope('service_provider')) { + const service_provider_sid = parseServiceProviderSid(req); + if (service_provider_sid === req.user.service_provider_sid) return next(); + } + res.status(403).json({ + status: 'fail', + message: 'insufficient privileges' + }); +}; + +module.exports = { + setupFreeTrial, + createTestCdrs, + createTestAlerts, + parseAccountSid, + parseServiceProviderSid, + hasAccountPermissions, + hasServiceProviderPermissions +}; diff --git a/lib/routes/api/voip-carriers.js b/lib/routes/api/voip-carriers.js index c1ece6e..bcd2136 100644 --- a/lib/routes/api/voip-carriers.js +++ b/lib/routes/api/voip-carriers.js @@ -1,15 +1,18 @@ const router = require('express').Router(); const {DbErrorBadRequest, DbErrorUnprocessableRequest} = require('../../utils/errors'); const VoipCarrier = require('../../models/voip-carrier'); +const {promisePool} = require('../../db'); const decorate = require('./decorate'); -const preconditions = { - 'add': validate, - 'update': validate, - 'delete': noActiveAccounts -}; +const sysError = require('../error'); -async function validate(req) { +const validate = async(req) => { const {lookupAppBySid, lookupAccountBySid} = req.app.locals; + + /* account level user can only act on carriers associated to his/her account */ + if (req.user.hasAccountAuth) { + req.body.account_sid = req.user.account_sid; + } + if (req.body.application_sid && !req.body.account_sid) { throw new DbErrorBadRequest('account_sid missing'); } @@ -24,14 +27,71 @@ async function validate(req) { const account = await lookupAccountBySid(req.body.account_sid); if (!account) throw new DbErrorBadRequest('unknown account_sid'); } -} +}; -/* can not delete a voip provider if it has any active phone numbers */ -async function noActiveAccounts(req, sid) { +const validateUpdate = async(req, sid) => { + const {lookupCarrierBySid} = req.app.locals; + await validate(req); + + if (req.user.hasAccountAuth) { + /* can only update carriers for the user's account */ + const carrier = await lookupCarrierBySid(sid); + if (carrier.account_sid != req.user.account_sid) { + throw new DbErrorUnprocessableRequest('carrier belongs to a different user'); + } + } +}; + +const validateDelete = async(req, sid) => { + const {lookupCarrierBySid} = req.app.locals; + if (req.user.hasAccountAuth) { + /* can only update carriers for the user's account */ + const carrier = await lookupCarrierBySid(sid); + if (carrier.account_sid != req.user.account_sid) { + throw new DbErrorUnprocessableRequest('carrier belongs to a different user'); + } + } + + /* can not delete a voip provider if it has any active phone numbers */ const activeAccounts = await VoipCarrier.getForeignKeyReferences('phone_numbers.voip_carrier_sid', sid); if (activeAccounts > 0) throw new DbErrorUnprocessableRequest('cannot delete voip carrier with active phone numbers'); -} -decorate(router, VoipCarrier, ['*'], preconditions); + /* remove all the sip and smpp gateways from the carrier first */ + await promisePool.execute('DELETE FROM sip_gateways WHERE voip_carrier_sid = ?', [sid]); + await promisePool.execute('DELETE FROM smpp_gateways WHERE voip_carrier_sid = ?', [sid]); +}; + +const preconditions = { + 'add': validate, + 'update': validateUpdate, + 'delete': validateDelete +}; + +decorate(router, VoipCarrier, ['add', 'update', 'delete'], preconditions); + +/* list */ +router.get('/', async(req, res) => { + const logger = req.app.locals.logger; + try { + const results = await VoipCarrier.retrieveAll(req.user.hasAccountAuth ? req.user.account_sid : null); + res.status(200).json(results); + } catch (err) { + sysError(logger, res, err); + } +}); + +/* retrieve */ +router.get('/:sid', async(req, res) => { + const logger = req.app.locals.logger; + try { + const account_sid = req.user.hasAccountAuth ? req.user.account_sid : null; + const results = await VoipCarrier.retrieve(req.params.sid, account_sid); + if (results.length === 0) return res.status(404).end(); + return res.status(200).json(results[0]); + } + catch (err) { + sysError(logger, res, err); + } +}); module.exports = router; diff --git a/lib/routes/api/webhooks.js b/lib/routes/api/webhooks.js new file mode 100644 index 0000000..6fe684d --- /dev/null +++ b/lib/routes/api/webhooks.js @@ -0,0 +1,21 @@ +const router = require('express').Router(); +const Webhook = require('../../models/webhook'); +const decorate = require('./decorate'); +const sysError = require('../error'); + +decorate(router, Webhook, ['add']); + +/* retrieve */ +router.get('/:sid', async(req, res) => { + const logger = req.app.locals.logger; + try { + const results = await Webhook.retrieve(req.params.sid); + if (results.length === 0) return res.status(404).end(); + return res.status(200).json(results[0]); + } + catch (err) { + sysError(logger, res, err); + } +}); + +module.exports = router; diff --git a/lib/routes/error.js b/lib/routes/error.js new file mode 100644 index 0000000..fb7b2f5 --- /dev/null +++ b/lib/routes/error.js @@ -0,0 +1,24 @@ +const {DbErrorBadRequest, DbErrorUnprocessableRequest, DbErrorForbidden} = require('../utils/errors'); + +function sysError(logger, res, err) { + if (err instanceof DbErrorBadRequest) { + logger.info(err, 'invalid client request'); + return res.status(400).json({msg: err.message}); + } + if (err instanceof DbErrorUnprocessableRequest) { + logger.info(err, 'unprocessable request'); + return res.status(422).json({msg: err.message}); + } + if (err instanceof DbErrorForbidden) { + logger.info(err, 'forbidden'); + return res.status(403).json({msg: err.message}); + } + if (err.code === 'ER_DUP_ENTRY') { + logger.info(err, 'duplicate entry on insert'); + return res.status(422).json({msg: err.message}); + } + logger.error(err, 'Database error'); + res.status(500).json({msg: err.message}); +} + +module.exports = sysError; diff --git a/lib/routes/index.js b/lib/routes/index.js index 037c705..8d32394 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -4,10 +4,12 @@ const YAML = require('yamljs'); const path = require('path'); const swaggerDocument = YAML.load(path.resolve(__dirname, '../swagger/swagger.yaml')); const api = require('./api'); +const stripe = require('./stripe'); const routes = express.Router(); routes.use('/v1', api); +routes.use('/stripe', stripe); routes.use('/swagger', swaggerUi.serve); routes.get('/swagger', swaggerUi.setup(swaggerDocument)); diff --git a/lib/routes/stripe/index.js b/lib/routes/stripe/index.js new file mode 100644 index 0000000..b0a3dfb --- /dev/null +++ b/lib/routes/stripe/index.js @@ -0,0 +1,5 @@ +const router = require('express').Router(); + +router.use('/webhook', require('./webhook')); + +module.exports = router; diff --git a/lib/routes/stripe/webhook.js b/lib/routes/stripe/webhook.js new file mode 100644 index 0000000..c0223e3 --- /dev/null +++ b/lib/routes/stripe/webhook.js @@ -0,0 +1,71 @@ +const router = require('express').Router(); +//const debug = require('debug')('jambonz:api-server'); +const Account = require('../../models/account'); +const {retrieveSubscription} = require('../../utils/stripe-utils'); +const stripeFactory = require('stripe'); +const express = require('express'); +const sysError = require('../error'); + +/** Invoice events */ +const handleInvoicePaymentSucceeded = async(logger, obj) => { + const {subscription} = obj; + logger.debug({obj}, `payment for ${obj.billing_reason} succeeded`); + const sub = await retrieveSubscription(logger, subscription); + if ('active' === sub.status) { + const {account_sid} = sub.metadata; + if (await Account.activateSubscription(logger, account_sid, sub.id, + 'subscription_create' === obj.billing_reason ? 'upgrade to paid plan' : 'change plan details')) { + logger.info(`handleInvoicePaymentSucceeded: activated subscription for account ${account_sid}`); + } + } +}; + +/** + * Two cases: + * (1) A subscription renewal fails. In this case we deactivate subscription + * and the customer is down until they provide payment. + * (2) A customer adds capacity during the month, and the pro-rated amount fails. + * In this case, we leave the new subscription in a pending state + * The customer continues (for the rest of the month at least) at + * previous capacity levels. + */ + +const handleInvoicePaymentFailed = async(logger, obj) => { + const {subscription} = obj; + const sub = await retrieveSubscription(logger, subscription); + logger.debug({obj}, `payment for ${obj.billing_reason} failed, subscription status is ${sub.status}`); + const {account_sid} = sub.metadata; + if (await Account.deactivateSubscription(logger, account_sid, 'payment failed')) { + logger.info(`handleInvoicePaymentFailed: deactivated subscription for account ${account_sid}`); + } +}; + +const handleInvoiceEvents = async(logger, evt) => { + if (evt.type === 'invoice.payment_succeeded') handleInvoicePaymentSucceeded(logger, evt.data.object); + else if (evt.type === 'invoice.payment_failed') handleInvoicePaymentFailed(logger, evt.data.object); +}; + + +router.post('/', express.raw({type: 'application/json'}), async(req, res) => { + const {logger} = req.app.locals; + const sig = req.get('stripe-signature'); + + let evt; + try { + if (!process.env.STRIPE_WEBHOOK_SECRET) throw new Error('missing webhook secret'); + const stripe = stripeFactory(process.env.STRIPE_API_KEY); + evt = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); + res.sendStatus(204); + } catch (err) { + sysError(logger, res, err); + } + + /* process event */ + logger.info(`received webhook: ${evt.type}`); + if (evt.type.startsWith('invoice.')) handleInvoiceEvents(logger, evt); + else { + logger.debug(evt, 'unhandled stripe webook'); + } +}); + +module.exports = router; diff --git a/lib/swagger/swagger.yaml b/lib/swagger/swagger.yaml index a38069f..5017819 100644 --- a/lib/swagger/swagger.yaml +++ b/lib/swagger/swagger.yaml @@ -12,6 +12,88 @@ servers: - url: /v1 description: development server paths: + /BetaInviteCodes: + post: + summary: generate one or more beta invite codes + operationId: generateInviteCode + requestBody: + content: + application/json: + schema: + type: object + properties: + count: + type: number + format: integer + responses: + 200: + description: invite codes successfully generated + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - ok + - failed + added: + type: number + format: integer + required: + - status + - added + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /PredefinedCarriers: + get: + summary: get a list of predefined carriers + operationId: listPredefinedCarriers + responses: + 200: + description: list of predefined carriers + content: + application/json: + schema: + type: + array + items: + $ref: '#/components/schemas/PredefinedCarrier' + /Accounts/{AccountSid}/PredefinedCarriers/{PredefinedCarrierSid}: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: PredefinedCarrierSid + in: path + required: true + schema: + type: string + format: uuid + post: + summary: add a VoiPCarrier to an account based on PredefinedCarrier template + operationId: createVoipCarrierFromTemplate + responses: + 201: + description: voip carrier successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' /Sbcs: post: summary: add an SBC address @@ -46,7 +128,7 @@ paths: $ref: '#/components/schemas/GeneralError' 500: - description: bad request + description: system error content: application/json: schema: @@ -96,8 +178,66 @@ paths: description: sbc address deleted 404: description: sbc address not found - - + /Smpps: + get: + summary: retrieve public IP addresses of the jambonz smpp servers + operationId: listSmpps + parameters: + - in: query + name: service_provider_sid + required: false + schema: + type: string + description: return only the smpp servers operated for the sole use of this service provider + responses: + 200: + description: list of smpp server addresses + content: + application/json: + schema: + type: array + items: + properties: + ipv4: + type: string + description: ip address of one of our Sbcs + port: + type: number + use_tls: + type: boolean + is_primary: + type: boolean + required: + - ipv4 + - port + - use_tls + - is_primary + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /InviteCodes: + post: + summary: validate an invite code + operationId: validateInviteCode + requestBody: + content: + application/json: + schema: + type: object + properties: + code: + type: string + required: + - code + responses: + 204: + description: code successfully validated + 404: + description: code not found + /ApiKeys: post: summary: create an api key @@ -152,10 +292,9 @@ paths: description: api key deleted 404: description: api key or account not found - - /login: + /signin: post: - summary: login a user and receive an api token + summary: sign in using email and password operationId: loginUser requestBody: content: @@ -163,28 +302,213 @@ paths: schema: type: object properties: - username: + email: type: string - description: login username password: type: string - description: login password - required: - - username - - password responses: 200: - description: login succeeded + description: successfully signed in content: application/json: - schema: - $ref: '#/components/schemas/Login' + schema: + $ref: '#/components/schemas/UserProfile' + example: + user_sid: 3192af15-8260-439f-939d-184613f6bbfb + account_sid: 7be2d35a-da6e-4faf-a382-1b37a571c7fd + name: Dave Horton + email: daveh@drachtio.org + provider: github + provider_userid: davehorton + pristine: true + account_validated: true + scope: read-write 403: - description: login failed + description: Invalid password + 404: + description: email not found + 500: + description: system error content: application/json: schema: $ref: '#/components/schemas/GeneralError' + /logout: + post: + summary: log out and deactivate jwt + operationId: logoutUser + responses: + 204: + description: user logged out + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /forgot-password: + post: + summary: send link to reset password + operationId: forgotPassword + requestBody: + content: + application/json: + schema: + type: object + properties: + email: + type: string + responses: + 204: + description: email sent + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + msg: service_provider_sid is missing + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /change-password: + post: + summary: changePassword + operationId: changePassword + requestBody: + content: + application/json: + schema: + type: object + properties: + old_password: + type: string + new_password: + type: string + responses: + 204: + description: password changed + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + msg: service_provider_sid is missing + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /register: + post: + summary: create a new user and account + operationId: registerUser + requestBody: + content: + application/json: + schema: + type: object + properties: + service_provider_sid: + type: string + example: 2708b1b3-2736-40ea-b502-c53d8396247f + provider: + type: string + enum: + - github + - google + - twitter + - local + example: github + email: + type: string + password: + type: string + email_activation_code: + type: string + oauth2_code: + type: string + example: f82659563e061e7347de + oauth2_state: + type: string + example: 386d2e990ad + oauth2_client_id: + type: string + example: a075a5889264b8fbc831 + oauth2_redirect_uri: + type: string + example: https://localhost:3000/oauth-gh-callback + required: + - service_provider_sid + - provider + responses: + 200: + description: user and account created + content: + application/json: + schema: + $ref: '#/components/schemas/UserProfile' + example: + user_sid: 3192af15-8260-439f-939d-184613f6bbfb + account_sid: 7be2d35a-da6e-4faf-a382-1b37a571c7fd + name: Dave Horton + email: daveh@drachtio.org + provider: github + provider_userid: davehorton + pristine: true + account_validated: true + scope: read-write + + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + msg: service_provider_sid is missing + 422: + description: User exists + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + msg: invalid service_provider_sid + 403: + description: invalid/expired oauth2 code + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + example: + msg: code is expired or invalid + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Invoices: + get: + summary: retrieve upcoming invoice for customer + operationId: retrieveInvoice + responses: + 200: + description: upcoming invoice + content: + application/json: + schema: + type: object + 404: + description: Not Found 500: description: system error content: @@ -201,7 +525,7 @@ paths: schema: type: string put: - summary: update a user password + summary: update user information operationId: updateUser requestBody: content: @@ -209,22 +533,21 @@ paths: schema: type: object properties: + name: + type: string + email: + type: string + email_activation_code: + type: string old_password: type: string description: existing password, which is to be replaced new_password: type: string description: new password - required: - - old_password - - new_password responses: - 200: - description: password successfully changed - content: - application/json: - schema: - $ref: '#/components/schemas/Login' + 204: + description: user updated 403: description: password change failed content: @@ -237,6 +560,315 @@ paths: application/json: schema: $ref: '#/components/schemas/GeneralError' + /Users/me: + get: + summary: retrieve details about logged-in user and associated account + operationId: getMyDetails + responses: + 200: + description: full-ish detail about the logged-in user and account + content: + application/json: + schema: + $ref: '#/components/schemas/UserAndAccountDetail' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + + /Availability: + get: + summary: check if a limited-availability entity such as a subdomain, email or phone number is already in use + operationId: checkAvailability + parameters: + - in: query + name: type + required: true + schema: + type: string + enum: + - email + - phone + - subdomain + example: subdomain + - in: query + name: value + required: true + schema: + type: string + example: mycorp.sip.jambonz.us + responses: + 200: + description: indicates whether value is already in use + content: + application/json: + schema: + type: object + properties: + available: + type: boolean + description: true if value requested is available + required: + - available + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Prices: + get: + summary: list all prices + operationId: listPrices + responses: + 200: + description: price listing + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + /StripeCustomerId: + get: + summary: retrieve stripe customer id for an account, creating if necessary + operationId: getStripeCustomerId + responses: + 200: + description: customer successfully provisioned or retrieved + content: + application/json: + schema: + type: object + properties: + stripe_customer_id: + type: string + required: + - stripe_customer_id + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Subscriptions: + get: + summary: get subscription details from stripe + operationId: getSubscription + responses: + 200: + description: stripe subscription entity + content: + application/json: + schema: + type: object + post: + summary: create or modify subscriptions in Stripe for a customer + operationId: manageSubscription + requestBody: + content: + application/json: + schema: + type: object + properties: + action: + type: string + enum: + - upgrade-to-paid + - downgrade-to-free + - update-payment-method + - update-quantities + payment_method_id: + type: string + dry_run: + type: boolean + products: + type: array + items: + properties: + product_sid: + type: string + price_id: + type: string + quantity: + type: number + format: integer + required: + - action + responses: + 201: + description: subscription successfully created + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - success + - card error + - action required + customer_id: + type: string + client_secret: + type: string + required: + - status + - customer_id + 400: + description: bad request + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + + /ActivationCode: + post: + summary: send an activation code to the user + operationId: sendActivationCode + requestBody: + content: + application/json: + schema: + type: object + properties: + code: + type: string + description: activation code + example: A74DF + user_sid: + type: string + format: uuid + description: identifies user to send to + type: + type: string + enum: + - email + - phone + value: + type: string + description: the new email or phone number to be activated + responses: + 204: + description: activation code successfully sent + 404: + description: User Not Found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /ActivationCode/{Code}: + parameters: + - name: Code + in: path + required: true + style: simple + explode: false + schema: + type: string + put: + summary: validate an activation code + operationId: validateActivationCode + requestBody: + content: + application/json: + schema: + type: object + properties: + user_sid: + type: string + format: uuid + description: identifies user to send to + type: + type: string + enum: + - email + - phone + responses: + 204: + description: activation code validated + 404: + description: User or activation code Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /AccountTest/:ServiceProviderSid: + parameters: + - name: ServiceProviderSid + in: path + required: true + style: simple + explode: false + schema: + type: string + get: + summary: get test phone numbers and applications + operationId: getTestData + responses: + 200: + description: test data for this service provider + content: + application/json: + schema: + type: object + properties: + phonenumbers: + type: array + items: + type: string + applications: + type: array + items: + $ref: '#/components/schemas/Application' + 404: + description: Service Provider Not Found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + + /Webhooks/{WebhookSid}: + parameters: + - name: WebhookSid + in: path + required: true + style: simple + explode: false + schema: + type: string + get: + summary: retrieve webhook + operationId: getWebhook + responses: + 200: + description: webhook found + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + 404: + description: webhook not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /VoipCarriers: post: summary: create voip carrier @@ -254,6 +886,10 @@ paths: description: type: string example: my US sip trunking provider + account_sid: + type: string + application_sid: + type: string e164_leading_plus: type: boolean description: whether a leading + is required on INVITEs to this provider @@ -273,6 +909,18 @@ paths: type: string description: sip password to authenticate with, if registration is required example: bar + tech_prefix: + type: string + description: prefix to be applied to the called number for outbound call attempts + inbound_auth_username: + type: string + description: challenge inbound calls with this username/password if supplied + inbound_auth_password: + type: string + description: challenge inbound calls with this username/password if supplied + diversion: + type: string + description: Diversion header or phone number to apply to outbound calls required: - name responses: @@ -460,6 +1108,13 @@ paths: get: summary: list sip gateways operationId: listSipGateways + parameters: + - in: query + name: voip_carrier_sid + required: true + schema: + type: string + description: return only the SipGateways operated for this VoipCarrier responses: 200: description: list of sip gateways @@ -507,7 +1162,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VoipCarrier' + $ref: '#/components/schemas/SipGateway' 404: description: sip gateway not found 500: @@ -541,7 +1196,146 @@ paths: application/json: schema: $ref: '#/components/schemas/GeneralError' - + /SmppGateways: + post: + summary: create smpp gateway + operationId: createSmppGateway + requestBody: + content: + application/json: + schema: + type: object + properties: + voip_carrier_sid: + type: string + description: voip carrier that provides this gateway + format: uuid + ipv4: + type: string + port: + type: number + netmask: + type: number + inbound: + type: boolean + outbound: + type: boolean + is_primary: + type: boolean + use_tls: + type: boolean + required: + - voip_carrier_sid + - ipv4 + responses: + 201: + description: smpp gateway successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 422: + description: unprocessable entity + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + get: + summary: list smpp gateways + operationId: listSmppGateways + responses: + 200: + description: list of smpp gateways + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SmppGateway' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /SmppGateways/{SmppGatewaySid}: + parameters: + - name: SmppGatewaySid + in: path + required: true + style: simple + explode: false + schema: + type: string + delete: + summary: delete a smpp gateway + operationId: deleteSmppGateway + responses: + 204: + description: smpp gateway successfully deleted + 404: + description: smpp gateway not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + get: + summary: retrieve smpp gateway + operationId: getSmppGateway + responses: + 200: + description: smpp gateway found + content: + application/json: + schema: + $ref: '#/components/schemas/SmppGateway' + 404: + description: smpp gateway not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + put: + summary: update sip gateway + operationId: updateSmppGateway + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SmppGateway' + responses: + 204: + description: smpp gateway updated + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 404: + description: smpp gateway not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' /PhoneNumbers: post: summary: provision a phone number into inventory from a Voip Carrier @@ -826,6 +1620,235 @@ paths: schema: $ref: '#/components/schemas/GeneralError' + /ServiceProviders/{ServiceProviderSid}/Accounts: + parameters: + - name: ServiceProviderSid + in: path + required: true + schema: + type: string + format: uuid + get: + summary: get all accounts for a service provider + operationId: getServiceProviderAccounts + responses: + 200: + description: account listing + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Account' + 403: + description: unauthorized + 404: + description: service provider not found + + /ServiceProviders/{ServiceProviderSid}/VoipCarriers: + parameters: + - name: ServiceProviderSid + in: path + required: true + schema: + type: string + format: uuid + get: + summary: get all carriers for a service provider + operationId: getServiceProviderCarriers + responses: + 200: + description: account listing + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VoipCarrier' + 403: + description: unauthorized + 404: + description: service provider not found + post: + summary: create a carrier + operationId: createCarrierForServiceProvider + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VoipCarrier' + responses: + 201: + description: service provider successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 404: + description: service provider not found + /ServiceProviders/{ServiceProviderSid}/PredefinedCarriers/{PredefinedCarrierSid}: + parameters: + - name: ServiceProviderSid + in: path + required: true + schema: + type: string + format: uuid + - name: PredefinedCarrierSid + in: path + required: true + schema: + type: string + format: uuid + post: + summary: add a VoiPCarrier to a service provider based on PredefinedCarrier template + operationId: createVoipCarrierFromTemplate + responses: + 201: + description: voip carrier successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + + /ServiceProviders/{ServiceProviderSid}/SpeechCredentials/: + parameters: + - name: ServiceProviderSid + in: path + required: true + schema: + type: string + format: uuid + post: + summary: create a speech credential for a service provider + operationId: addSpeechCredentialForSeerviceProvider + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredential' + responses: + 201: + description: speech credential successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 404: + description: credential not found + /ServiceProviders/{ServiceProviderSid}/SpeechCredentials/{SpeechCredentialSid}: + parameters: + - name: ServiceProviderSid + in: path + required: true + schema: + type: string + format: uuid + - name: SpeechCredentialSid + in: path + required: true + schema: + type: string + format: uuid + get: + summary: get a specific speech credential + operationId: getSpeechCredential + responses: + 200: + description: retrieve speech credentials for a specified account + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredential' + 404: + description: credential not found + put: + summary: update a speech credential + operationId: updateSpeechCredential + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredentialUpdate' + responses: + 204: + description: credential successfully updated + 404: + description: credential not found + 422: + description: credential not found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + delete: + summary: delete a speech credential + operationId: deleteSpeechCredential + responses: + 204: + description: credential successfully deleted + 404: + description: credential not found + /ServiceProviders/{ServiceProviderSid}/SpeechCredentials/{SpeechCredentialSid}/test: + get: + summary: test a speech credential + operationId: testSpeechCredential + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: SpeechCredentialSid + in: path + required: true + schema: + type: string + format: uuid + responses: + 200: + description: credential test results + content: + application/json: + schema: + type: object + properties: + tts: + type: object + properties: + status: + type: string + enum: + - success + - fail + - not tested + reason: + type: string + stt: + type: object + properties: + status: + type: string + enum: + - success + - fail + reason: + type: string + 404: + description: credential not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' /MicrosoftTeamsTenants: post: summary: provision a customer tenant for MS Teams @@ -940,7 +1963,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Tenant' + $ref: '#/components/schemas/MsTeamsTenant' responses: 204: description: tenant updated @@ -1216,6 +2239,41 @@ paths: application/json: schema: $ref: '#/components/schemas/GeneralError' + /Accounts/{AccountSid}/WebhookSecret: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: regenerate + in: query + required: false + schema: + type: boolean + get: + summary: get webhook signing secret, regenerating if requested + operationId: getWebhookSecret + responses: + 200: + description: secret + content: + application/json: + schema: + type: object + properties: + webhook_secret: + type: string + 404: + description: account not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Accounts/{AccountSid}/ApiKeys: parameters: - name: AccountSid @@ -1245,6 +2303,445 @@ paths: schema: $ref: '#/components/schemas/GeneralError' + /Accounts/{AccountSid}/SipRealms/{SipRealm}: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: SipRealm + in: path + required: true + schema: + type: string + post: + summary: add or change the sip realm + operationId: createSipRealm + responses: + 204: + description: sip_realm updated and DNS entries successfully created + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + + /Accounts/{AccountSid}/SpeechCredentials: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + + post: + summary: add a speech credential + operationId: createSpeechCredential + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredential' + responses: + 201: + description: speech credential successfully created + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessfulAdd' + 400: + description: bad request + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + get: + summary: retrieve all speech credentials for an account + operationId: listSpeechCredentials + responses: + 200: + description: retrieve speech credentials for a specified account + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SpeechCredential' + 404: + description: account not found + /Accounts/{AccountSid}/SpeechCredentials/{SpeechCredentialSid}: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: SpeechCredentialSid + in: path + required: true + schema: + type: string + format: uuid + get: + summary: get a specific speech credential + operationId: getSpeechCredential + responses: + 200: + description: retrieve speech credentials for a specified account + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredential' + 404: + description: credential not found + put: + summary: update a speech credential + operationId: updateSpeechCredential + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SpeechCredentialUpdate' + responses: + 204: + description: credential successfully deleted + 404: + description: credential not found + 422: + description: credential not found + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + delete: + summary: delete a speech credential + operationId: deleteSpeechCredential + responses: + 204: + description: credential successfully deleted + 404: + description: credential not found + /Accounts/{AccountSid}/SpeechCredentials/{SpeechCredentialSid}/test: + get: + summary: test a speech credential + operationId: testSpeechCredential + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - name: SpeechCredentialSid + in: path + required: true + schema: + type: string + format: uuid + responses: + 200: + description: credential test results + content: + application/json: + schema: + type: object + properties: + tts: + type: object + properties: + status: + type: string + enum: + - success + - fail + - not tested + reason: + type: string + stt: + type: object + properties: + status: + type: string + enum: + - success + - fail + reason: + type: string + 404: + description: credential not found + 500: + description: system error + content: + application/json: + schema: + $ref: '#/components/schemas/GeneralError' + /Accounts/{AccountSid}/RecentCalls: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - in: query + name: page + required: true + schema: + type: number + format: integer + description: page number of data to retrieve + - in: query + name: count + required: true + schema: + type: number + format: integer + description: number of rows to retrieve in each page set + - in: query + name: days + required: false + schema: + type: number + format: integer + description: number of days back to retrieve, must be ge 1 and le 30 + - in: query + name: start + required: false + schema: + type: string + format: date-time + description: start date to retrieve + - in: query + name: end + required: false + schema: + type: string + format: date-time + description: end date to retrieve + - in: query + name: answered + required: false + schema: + type: string + enum: + - true + - false + description: retrieve only answered calls + - in: query + name: direction + required: false + schema: + type: string + enum: + - inbound + - outbound + get: + summary: retrieve recent calls for an account + operationId: listRecentCalls + responses: + 200: + description: retrieve recent call records for a specified account + content: + application/json: + schema: + type: object + properties: + total: + type: number + format: integer + description: total number of records in that database that match the filter criteria + batch: + type: number + format: integer + description: total number of records returned in this page set + page: + type: number + format: integer + description: page number that was requested, and is being returned + data: + type: array + items: + type: object + properties: + account_sid: + type: string + format: uuid + call_sid: + type: string + format: uuid + from: + type: string + to: + type: string + answered: + type: boolean + sip_call_id: + type: string + sip_status: + type: number + format: integer + duration: + type: number + format: integer + attempted_at: + type: number + format: integer + answered_at: + type: number + format: integer + terminated_at: + type: number + format: integer + termination_reason: + type: string + host: + type: string + remote_host: + type: string + direction: + type: string + enum: + - inbound + - outbound + trunk: + type: string + required: + - account_sid + - call_sid + - attempted_at + - terminated_at + - answered + - direction + - from + - to + - sip_status + - duration + 404: + description: account not found + /Accounts/{AccountSid}/Alerts: + parameters: + - name: AccountSid + in: path + required: true + schema: + type: string + format: uuid + - in: query + name: page + required: true + schema: + type: number + format: integer + description: page number of data to retrieve + - in: query + name: count + required: true + schema: + type: number + format: integer + description: number of rows to retrieve in each page set + - in: query + name: days + required: false + schema: + type: number + format: integer + description: number of days back to retrieve, must be ge 1 and le 30 + - in: query + name: start + required: false + schema: + type: string + format: date-time + description: start date to retrieve + - in: query + name: end + required: false + schema: + type: string + format: date-time + description: end date to retrieve + - in: query + name: alert_type + required: false + schema: + type: string + enum: + - webhook-failure + - webhook-connection-failure + - webhook-auth-failure + - no-tts + - no-stt + - tts-failure + - stt-failure + - no-carrier + - call-limit + - device-limit + - api-limit + get: + summary: retrieve alerts for an account + operationId: listAlerts + responses: + 200: + description: retrieve alerts for a specified account + content: + application/json: + schema: + type: object + properties: + total: + type: number + format: integer + description: total number of records in that database that match the filter criteria + batch: + type: number + format: integer + description: total number of records returned in this page set + page: + type: number + format: integer + description: page number that was requested, and is being returned + data: + type: array + items: + type: object + properties: + time: + type: string + format: date-time + account_sid: + type: string + format: uuid + alert_type: + type: string + message: + type: string + detail: + type: string + required: + - time + - account_sid + - alert_type + - message + 404: + description: account not found /Applications: post: summary: create application @@ -1681,6 +3178,15 @@ components: description: authentication webhook for registration ms_teams_fqdn: type: string + test_number: + type: string + description: used for inbound testing for accounts on free plan + test_application_name: + type: string + description: name of test application that can be used for new signups + test_application_sid: + type: string + description: identifies test application that can be used for new signups required: - service_provider_sid - name @@ -1694,6 +3200,12 @@ components: type: string description: type: string + account_sid: + type: string + format: uuid + application_sid: + type: string + format: uuid e164_leading_plus: type: boolean requires_register: @@ -1704,6 +3216,27 @@ components: type: string register_password: type: string + tech_prefix: + type: string + inbound_auth_username: + type: string + inbound_auth_password: + type: string + diversion: + type: string + is_active: + type: boolean + smpp_system_id: + type: string + smpp_password: + type: string + smpp_inbound_system_id: + type: string + smpp_inbound_password: + type: string + smpp_enquire_link_interval: + type: number + format: integer required: - voip_carrier_sid - name @@ -1717,11 +3250,11 @@ components: type: string port: type: number + netmask: + type: number voip_carrier_sid: type: string format: uuid - is_active: - type: boolean inbound: type: boolean outbound: @@ -1731,6 +3264,36 @@ components: - voip_carrier_sid - ipv4 - port + - netmask + SmppGateway: + type: object + properties: + smpp_gateway_sid: + type: string + format: uuid + ipv4: + type: string + port: + type: number + netmask: + type: number + voip_carrier_sid: + type: string + format: uuid + is_primary: + type: boolean + use_tls: + type: boolean + inbound: + type: boolean + outbound: + type: boolean + required: + - smpp_gateway_sid + - voip_carrier_sid + - ipv4 + - port + - netmask Account: type: object properties: @@ -1802,11 +3365,14 @@ components: type: string format: uuid expires_at: - type: dateTime + type: string + format: date-time created_at: - type: dateTime + type: string + format: date-time last_used: - type: dateTime + type: string + format: date-time required: - api_key_sid - token @@ -1853,6 +3419,9 @@ components: Webhook: type: object properties: + webhook_sid: + type: string + format: uuid url: type: string format: url @@ -1983,6 +3552,443 @@ components: - from - to example: {"from": "13394445678", "to": "16173333456", "text": "please call when you can"} + SpeechCredential: + properties: + speech_credential_sid: + type: string + format: uuid + account_sid: + type: string + format: uuid + vendor: + type: string + enum: + - google + - aws + service_key: + type: string + access_key_id: + type: string + secret_access_key: + type: string + aws_region: + type: string + last_used: + type: string + format: date-time + last_tested: + type: string + format: date-time + use_for_tts: + type: boolean + use_for_stt: + type: boolean + tts_tested_ok: + type: boolean + stt_tested_ok: + type: boolean + SpeechCredentialUpdate: + properties: + use_for_tts: + type: boolean + use_for_stt: + type: boolean + UserAndAccountDetail: + type: object + properties: + user: + type: object + properties: + user_sid: + type: string + name: + type: string + email: + type: string + phone: + type: string + service_provider_sid: + type: string + force_change: + type: boolean + provider: + type: string + provider_userid: + type: string + scope: + type: string + email_validated: + type: boolean + phone_validated: + type: string + account: + type: object + properties: + account_sid: + type: string + sip_realm: + type: string + service_provider_sid: + type: string + registration_hook_sid: + type: string + device_calling_application_sid: + type: string + is_active: + type: boolean + created_at: + type: string + format: date-time + testapp: + type: object + properties: + application_sid: + type: string + name: + type: string + service_provider_sid: + type: string + account_sid: + type: string + call_hook_sid: + type: string + call_status_hook_sid: + type: string + messaging_hook_sid: + type: string + speech_synthesis_vendor: + type: string + speech_synthesis_language: + type: string + speech_synthesis_voice: + type: string + speech_recognizer_vendor: + type: string + speech_recognizer_language: + type: string + balance: + type: object + properties: + currency: + type: string + enum: + - USD + - EUR + balance: + type: number + last_updated_at: + type: string + format: date-time + created_at: + type: string + format: date-time + last_transaction_id: + type: string + capacities: + type: object + properties: + effective_start_date: + type: string + format: date-time + effective_end_date: + type: string + format: date-time + limit_sessions: + type: integer + limit_registrations: + type: integer + api_keys: + type: array + items: + type: object + properties: + token: + type: string + last_used: + type: string + format: date-time + created_at: + type: string + format: date-time + products: + type: array + items: + type: object + properties: + name: + type: string + description: + type: string + category: + type: string + quantity: + type: string + in_starter_set: + type: boolean + product_sid: + type: string + effective_start_date: + type: string + format: date-time + effective_end_date: + type: string + format: date-time + UserProfile: + type: object + properties: + user_sid: + type: string + example: a5bce31e-a028-45cd-94c4-f121b72fec61 + account_sid: + type: string + example: a5bce31e-a028-45cd-94c4-f121b72fec61 + is_active: + type: boolean + description: indicates whether account is active + example: true + name: + type: string + description: full user name + example: Dave Horton + email: + type: string + description: email associated with user + example: daveh@drachtio.org + phone: + type: string + description: phone associated with user + provider: + type: string + description: authentication provider + enum: + - github + - google + - local + example: github + scope: + type: string + description: scope of user permissions + enum: + - read-only + - read-write + example: read-write + pristine: + type: boolean + description: true if account was newly created + example: true + email_validated: + type: boolean + description: indicates whether user has validated their email address + example: true + phone_validated: + type: boolean + description: indicates whether user has validated their mobile phone + example: true + tutorial_completion: + type: number + description: bitmask indicating which tutorials have been completed + example: 1 + jwt: + type: string + description: json web token to be used as bearer token in API requests + required: + - user_sid + - provider + - name + - email + - scope + - account_validated + - pristine + - jwt + - is_active + Charge: + type: object + properties: + charge_sid: + type: string + format: uuid + account_sid: + type: string + format: uuid + application_sid: + type: string + format: uuid + billed_at: + type: string + format: date-time + billed_activity: + type: string + enum: + - inbound-call + - outbound-call + - inbound-sms + - outbound-sms + - inbound-mms + - outbound-mms + - tts + - stt + call_billing_record_sid: + type: string + format: uuid + message_record_sid: + type: string + format: uuid + call_secs_billed: + type: number + format: integer + tts_chars_billed: + type: number + format: integer + stt_secs_billed: + type: number + format: integer + amount_charged: + type: number + format: double + required: + - account_sid + - billed_activity + - amount_charged + RecentCalls: + type: object + properties: + account_sid: + type: string + format: uuid + call_sid: + type: string + format: uuid + from: + type: string + to: + type: string + answered: + type: boolean + sip_call_id: + type: string + sip_status: + type: number + format: integer + duration: + type: number + format: integer + attempted_at: + type: number + format: integer + answered_at: + type: number + format: integer + terminated_at: + type: number + format: integer + termination_reason: + type: string + host: + type: string + remote_host: + type: string + direction: + type: string + enum: + - inbound + - outbound + trunk: + type: string + required: + - account_sid + - call_sid + - attempted_at + - terminated_at + - answered + - direction + - from + - to + - sip_status + - duration + Alert: + type: object + properties: + alert_sid: + type: string + format: uuid + call_sid: + type: string + format: uuid + account_sid: + type: string + format: uuid + application_sid: + type: string + format: uuid + occurred_at: + type: string + format: date-time + alert_type: + type: string + enum: + - webhook-failed + - bad-application-syntax + - speech-operation-failed + - limit-exceeded + details: + type: string + required: + - alert_sid + - account_sid + - occurred_at + - alert_type + Product: + type: object + properties: + product_sid: + type: string + name: + type: string + description: + type: string + unit_label: + type: string + category: + type: string + required: + - product_sid + - name + - unit_label + - category + PredefinedCarrier: + type: object + properties: + predefined_carrier_sid: + type: string + format: uuid + name: + type: string + requires_static_ip: + type: boolean + e164_leading_plus: + type: boolean + requires_register: + type: boolean + register_username: + type: string + register_sip_realm: + type: string + register_password: + type: string + tech_prefix: + type: string + inbound_auth_username: + type: string + inbound_auth_password: + type: string + diversion: + type: string + required: + - predefined_carrier_sid + - name + - requires_static_ip + - e164_leading_plus + - requires_register security: - bearerAuth: [] \ No newline at end of file diff --git a/lib/utils/dns-utils.js b/lib/utils/dns-utils.js new file mode 100644 index 0000000..9ef2d36 --- /dev/null +++ b/lib/utils/dns-utils.js @@ -0,0 +1,123 @@ +if (!process.env.JAMBONES_HOSTING) return; + +const bent = require('bent'); +const crypto = require('crypto'); +const assert = require('assert'); +const domains = new Map(); +const debug = require('debug')('jambonz:api-server'); + +const checkAsserts = () => { + assert.ok(process.env.DME_API_KEY, 'missing env DME_API_KEY for dns operations'); + assert.ok(process.env.DME_API_SECRET, 'missing env DME_API_SECRET for dns operations'); + assert.ok(process.env.DME_BASE_URL, 'missing env DME_BASE_URL for dns operations'); +}; + +const createAuthHeaders = () => { + const now = (new Date()).toUTCString(); + const hash = crypto.createHmac('SHA1', process.env.DME_API_SECRET); + hash.update(now); + return { + 'x-dnsme-apiKey': process.env.DME_API_KEY, + 'x-dnsme-requestDate': now, + 'x-dnsme-hmac': hash.digest('hex') + }; +}; + +const getDnsDomainId = async(logger, name) => { + checkAsserts(); + const headers = createAuthHeaders(); + const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers); + try { + const result = await get('/dns/managed'); + debug(result, 'getDnsDomainId: all domains'); + if (Array.isArray(result.data)) { + const domain = result.data.find((o) => o.name === name); + if (domain) return domain.id; + debug(`getDnsDomainId: failed to find domain ${name}`); + } + } catch (err) { + logger.error({err}, 'Error retrieving domains'); + } +}; + +/** + * Add the DNS records for a given subdomain + * We will add an A record and an SRV record for each SBC public IP address + * Note: this assumes we have manually added DNS A records: + * sbc01.root.domain, sbc0.root.domain, etc to dnsmadeeasy + */ +const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => { + checkAsserts(); + try { + if (!domains.has(domain)) { + const domainId = await getDnsDomainId(logger, domain); + if (!domainId) return false; + domains.set(domain, domainId); + } + const domainId = domains.get(domain); + + value = Array.isArray(value) ? value : [value]; + const a_records = value.map((v) => { + return { + type: 'A', + gtdLocation: 'DEFAULT', + name, + value: v, + ttl + }; + }); + const srv_records = [ + { + type: 'SRV', + gtdLocation: 'DEFAULT', + name: `_sip._udp.${name}`, + value: `${name}`, + port: 5060, + priority: 10, + weight: 100, + ttl + } + ]; + const headers = createAuthHeaders(); + const records = [...a_records, ...srv_records]; + const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers); + logger.debug({records}, 'Attemting to create dns records'); + const res = await post(`/dns/managed/${domainId}/records/createMulti`, + [...a_records, ...srv_records]); + + if (201 === res.statusCode) { + const str = await res.text(); + return JSON.parse(str); + } + logger.error({res}, 'Error creating records'); + } catch (err) { + logger.error({err}, 'Error retrieving domains'); + } +}; + +const deleteDnsRecords = async(logger, domain, recIds) => { + checkAsserts(); + const headers = createAuthHeaders(); + const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers); + try { + if (!domains.has(domain)) { + const domainId = await getDnsDomainId(logger, domain); + if (!domainId) return false; + domains.set(domain, domainId); + } + const domainId = domains.get(domain); + const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`; + await del(url); + return true; + } catch (err) { + console.error(err); + logger.error({err}, 'Error deleting records'); + } +}; + + +module.exports = { + getDnsDomainId, + createDnsRecords, + deleteDnsRecords +}; diff --git a/lib/utils/email-utils.js b/lib/utils/email-utils.js new file mode 100644 index 0000000..18a4663 --- /dev/null +++ b/lib/utils/email-utils.js @@ -0,0 +1,34 @@ +const formData = require('form-data'); +const Mailgun = require('mailgun.js'); +const mailgun = new Mailgun(formData); +const validateEmail = (email) => { + // eslint-disable-next-line max-len + const re = /^(([^<>()[\]\\.,;:\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 re.test(email); +}; + +const emailSimpleText = async(logger, to, subject, text) => { + const mg = mailgun.client({ + username: 'api', + key: process.env.MAILGUN_API_KEY + }); + if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!'); + if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!'); + + try { + const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, { + from: 'jambonz Support ', + to, + subject, + text + }); + logger.debug({res}, 'sent email'); + } catch (err) { + logger.info({err}, 'Error sending email'); + } +}; + +module.exports = { + validateEmail, + emailSimpleText +}; diff --git a/lib/utils/encrypt-decrypt.js b/lib/utils/encrypt-decrypt.js new file mode 100644 index 0000000..e2e5cae --- /dev/null +++ b/lib/utils/encrypt-decrypt.js @@ -0,0 +1,29 @@ +const crypto = require('crypto'); +const algorithm = 'aes-256-ctr'; +const iv = crypto.randomBytes(16); +const secretKey = crypto.createHash('sha256') + .update(String(process.env.JWT_SECRET)) + .digest('base64') + .substr(0, 32); + +const encrypt = (text) => { + const cipher = crypto.createCipheriv(algorithm, secretKey, iv); + const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); + const data = { + iv: iv.toString('hex'), + content: encrypted.toString('hex') + }; + return JSON.stringify(data); +}; + +const decrypt = (data) => { + const hash = JSON.parse(data); + const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex')); + const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]); + return decrpyted.toString(); +}; + +module.exports = { + encrypt, + decrypt +}; diff --git a/lib/utils/errors.js b/lib/utils/errors.js index 49f3532..417a8a4 100644 --- a/lib/utils/errors.js +++ b/lib/utils/errors.js @@ -16,8 +16,15 @@ class DbErrorUnprocessableRequest extends DbError { } } +class DbErrorForbidden extends DbError { + constructor(msg) { + super(msg); + } +} + module.exports = { DbError, DbErrorBadRequest, - DbErrorUnprocessableRequest + DbErrorUnprocessableRequest, + DbErrorForbidden }; diff --git a/lib/utils/free_plans.json b/lib/utils/free_plans.json new file mode 100644 index 0000000..4dca62b --- /dev/null +++ b/lib/utils/free_plans.json @@ -0,0 +1,22 @@ +{ + "trial": [ + { + "category": "voice_call_session", + "quantity": 20 + }, + { + "category": "device", + "quantity": 0 + } + ], + "free": [ + { + "category": "voice_call_session", + "quantity": 1 + }, + { + "category": "device", + "quantity": 1 + } + ] +} \ No newline at end of file diff --git a/lib/utils/oauth-utils.js b/lib/utils/oauth-utils.js new file mode 100644 index 0000000..2bb16f2 --- /dev/null +++ b/lib/utils/oauth-utils.js @@ -0,0 +1,112 @@ +const assert = require('assert'); +const bent = require('bent'); +const postJSON = bent('POST', 'json', 200); +const getJSON = bent('GET', 'json', 200); +const {emailSimpleText} = require('./email-utils'); +const {DbErrorForbidden} = require('../utils/errors'); + +const doGithubAuth = async(logger, payload) => { + assert.ok(process.env.GITHUB_CLIENT_SECRET, 'env var GITHUB_CLIENT_SECRET is required'); + + try { + /* exchange the code for an access token */ + const obj = await postJSON('https://github.com/login/oauth/access_token', { + client_id: payload.oauth2_client_id, + client_secret: process.env.GITHUB_CLIENT_SECRET, + code: payload.oauth2_code, + state: payload.oauth2_state, + redirect_uri: payload.oauth2_redirect_uri + }); + if (!obj.access_token) { + logger.error({obj}, 'Error retrieving access_token from github'); + if (obj.error === 'bad_verification_code') throw new Error('bad verification code'); + throw new Error(obj.error || 'error retrieving access_token'); + } + logger.debug({obj}, 'got response from github for access_token'); + + /* use the access token to get basic public info as well as primary email */ + const userDetails = await getJSON('https://api.github.com/user', null, { + Authorization: `Bearer ${obj.access_token}`, + Accept: 'application/json', + 'User-Agent': 'jambonz 1.0' + }); + + const emails = await getJSON('https://api.github.com/user/emails', null, { + Authorization: `Bearer ${obj.access_token}`, + Accept: 'application/json', + 'User-Agent': 'jambonz 1.0' + }); + const primary = emails.find((e) => e.primary); + if (primary) Object.assign(userDetails, { + email: primary.email, + email_validated: primary.validated + }); + + logger.info({userDetails}, 'retrieved user details from github'); + return userDetails; + } catch (err) { + logger.info({err}, 'Error authenticating via github'); + throw new DbErrorForbidden(err.message); + } +}; + +const doGoogleAuth = async(logger, payload) => { + assert.ok(process.env.GOOGLE_OAUTH_CLIENT_SECRET, 'env var GOOGLE_OAUTH_CLIENT_SECRET is required'); + + try { + /* exchange the code for an access token */ + const obj = await postJSON('https://oauth2.googleapis.com/token', { + client_id: payload.oauth2_client_id, + client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET, + code: payload.oauth2_code, + state: payload.oauth2_state, + redirect_uri: payload.oauth2_redirect_uri, + grant_type: 'authorization_code' + }); + if (!obj.access_token) { + logger.error({obj}, 'Error retrieving access_token from github'); + if (obj.error === 'bad_verification_code') throw new Error('bad verification code'); + throw new Error(obj.error || 'error retrieving access_token'); + } + logger.debug({obj}, 'got response from google for access_token'); + + /* use the access token to get basic public info as well as primary email */ + const userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, { + Authorization: `Bearer ${obj.access_token}`, + Accept: 'application/json', + 'User-Agent': 'jambonz 1.0' + }); + + logger.info({userDetails}, 'retrieved user details from google'); + return userDetails; + } catch (err) { + logger.info({err}, 'Error authenticating via google'); + throw new DbErrorForbidden(err.message); + } +}; +const doLocalAuth = async(logger, payload) => { + const {name, email, password, email_activation_code} = payload; + const text = `Hi there + + Welcome to jambonz! Your account activation code is ${email_activation_code} + + Best, + + The jambonz team`; + + if ('test' !== process.env.NODE_ENV || process.env.MAILGUN_API_KEY) { + await emailSimpleText(logger, email, 'Account activation code', text); + } + return { + name, + email, + password, + email_activation_code + }; +}; + +module.exports = { + doGithubAuth, + doGoogleAuth, + doLocalAuth +}; diff --git a/lib/utils/password-utils.js b/lib/utils/password-utils.js new file mode 100644 index 0000000..088c79d --- /dev/null +++ b/lib/utils/password-utils.js @@ -0,0 +1,24 @@ +const crypto = require('crypto'); +const { argon2i } = require('argon2-ffi'); +const util = require('util'); + +const getRandomBytes = util.promisify(crypto.randomBytes); + +const generateHashedPassword = async(password) => { + const salt = await getRandomBytes(32); + const passwordHash = await argon2i.hash(password, salt); + return passwordHash; +}; + +const verifyPassword = async(passwordHash, password) => { + const isCorrect = await argon2i.verify(passwordHash, password); + return isCorrect; +}; + +const hashString = (s) => crypto.createHash('md5').update(s).digest('hex'); + +module.exports = { + generateHashedPassword, + verifyPassword, + hashString +}; diff --git a/lib/utils/phone-number-utils.js b/lib/utils/phone-number-utils.js new file mode 100644 index 0000000..8098150 --- /dev/null +++ b/lib/utils/phone-number-utils.js @@ -0,0 +1,22 @@ +//const PNF = require('google-libphonenumber').PhoneNumberFormat; +//const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance(); + +const validateNumber = (number) => { + if (typeof number !== 'string') throw new Error('phone number must be a string'); + if (!/^\d+$/.test(number)) throw new Error('phone number must only include digits'); +}; + +const e164 = (number) => { + if (number.startsWith('+')) return number.slice(1); + return number; + /* + const num = phoneUtil.parseAndKeepRawInput(number, 'US'); + if (!phoneUtil.isValidNumber(num)) throw new Error(`not a valid US telephone number: ${number}`); + return phoneUtil.format(num, PNF.E164).slice(1); + */ +}; + +module.exports = { + validateNumber, + e164 +}; diff --git a/lib/utils/speech-utils.js b/lib/utils/speech-utils.js new file mode 100644 index 0000000..def6b66 --- /dev/null +++ b/lib/utils/speech-utils.js @@ -0,0 +1,60 @@ +const ttsGoogle = require('@google-cloud/text-to-speech'); +const sttGoogle = require('@google-cloud/speech').v1p1beta1; +const Polly = require('aws-sdk/clients/polly'); +const AWS = require('aws-sdk'); +const fs = require('fs'); + +const testGoogleTts = async(logger, credentials) => { + const client = new ttsGoogle.TextToSpeechClient({credentials}); + await client.listVoices(); +}; + +const testGoogleStt = async(logger, credentials) => { + const client = new sttGoogle.SpeechClient({credentials}); + const config = { + sampleRateHertz: 8000, + languageCode: 'en-US', + model: 'default', + }; + const audio = { + content: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`).toString('base64'), + }; + const request = { + config: config, + audio: audio, + }; + + // Detects speech in the audio file + const [response] = await client.recognize(request); + if (!Array.isArray(response.results) || 0 === response.results.length) { + throw new Error('failed to transcribe speech'); + } +}; + +const testAwsTts = (logger, credentials) => { + const polly = new Polly(credentials); + return new Promise((resolve, reject) => { + polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => { + if (err) return reject(err); + resolve(); + }); + }); +}; + +const testAwsStt = (logger, credentials) => { + const transcribeservice = new AWS.TranscribeService(credentials); + return new Promise((resolve, reject) => { + transcribeservice.listVocabularies((err, data) => { + if (err) return reject(err); + logger.info({data}, 'retrieved language models'); + resolve(); + }); + }); +}; + +module.exports = { + testGoogleTts, + testGoogleStt, + testAwsTts, + testAwsStt +}; diff --git a/lib/utils/stripe-utils.js b/lib/utils/stripe-utils.js new file mode 100644 index 0000000..b4bb005 --- /dev/null +++ b/lib/utils/stripe-utils.js @@ -0,0 +1,224 @@ +if (!process.env.JAMBONES_HOSTING) return; + +const assert = require('assert'); +assert.ok(process.env.STRIPE_API_KEY || process.env.NODE_ENV === 'test', + 'missing env STRIPE_API_KEY for billing operations'); +assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test', + 'missing env STRIPE_BASE_URL for billing operations'); + +const bent = require('bent'); +const formurlencoded = require('form-urlencoded').default; +const qs = require('qs'); +const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64'); +const basicAuth = () => { + const header = `Basic ${toBase64(process.env.STRIPE_API_KEY)}`; + return {Authorization: header}; +}; +const postForm = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'POST', 'string', + Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, basicAuth()), 200); +const getJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'GET', 'json', basicAuth(), 200); +const deleteJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'DELETE', 'json', basicAuth(), 200); +//const debug = require('debug')('jambonz:api-server'); + +const listProducts = async(logger) => await getJSON('/products?active=true'); + +const listPrices = async(logger) => await getJSON('/prices?active=true&expand[]=data.tiers&expand[]=data.product'); + +const retrievePricesForProduct = async(logger, id) => + await getJSON(`/prices?product=${id}&active=true&expand[]=data.tiers&expand[]=data.product`); + +const retrieveProduct = async(logger, id) => + await getJSON(`/products/${id}`); + + +const retrieveCustomer = async(logger, id) => + await getJSON(`/customers/${id}`); + +const retrieveUpcomingInvoice = async(logger, customer_id, subscription_id, items) => { + const params = Object.assign( + {customer: customer_id}, + subscription_id ? {subscription: subscription_id} : {}, + items ? {subscription_items: items} : {}); + const queryString = qs.stringify(params, {encode: false}); + logger.debug({params, qs}, 'retrieving upcoming invoice'); + return await getJSON(`/invoices/upcoming?${queryString}`); +}; + +const retrieveInvoice = async(logger, id) => + await getJSON(`/invoices/${id}`); + +const createCustomer = async(logger, account_sid, email, name) => { + const obj = { + email, + metadata: {account_sid} + }; + if (name) obj.name = name; + logger.debug({obj}, 'provisioning customer'); + const result = await postForm('/customers', formurlencoded(obj)); + return JSON.parse(result); +}; + +const updateCustomer = async(logger, id, obj) => { + logger.debug({obj}, `updating customer ${id}`); + const result = await postForm(`/customers/${id}`, formurlencoded(obj)); + return JSON.parse(result); +}; + +const deleteCustomer = async(logger, id) => + await deleteJSON(`/customers/${id}`); + +const attachPaymentMethod = async(logger, payment_method_id, customer_id) => { + const obj = { + customer: customer_id + }; + const result = await postForm(`/payment_methods/${payment_method_id}/attach`, + formurlencoded(obj)); + return JSON.parse(result); +}; + +const detachPaymentMethod = async(logger, payment_method_id) => { + const result = await postForm(`/payment_methods/${payment_method_id}/detach`); + return JSON.parse(result); +}; + +const createSubscription = async(logger, customer, metadata, items) => { + assert.ok(Array.isArray(items) && items.length > 0); + const obj = { + customer, + metadata, + items + }; + const result = await postForm('/subscriptions?expand[]=latest_invoice&expand[]=latest_invoice.payment_intent', + formurlencoded(obj)); + return JSON.parse(result); +}; + +/* +const deleteInvoiceItem = async(logger, id) => { + return JSON.parse(await deleteJSON(`/invoiceitems/${id}`)); +}; +*/ + +const updateSubscription = async(logger, id, items) => { + assert.ok(Array.isArray(items) && items.length > 0); + const obj = { + proration_behavior: 'always_invoice', + payment_behavior: 'pending_if_incomplete', + items + }; + const result = await postForm(`/subscriptions/${id}`, + formurlencoded(obj)); + return JSON.parse(result); +}; + + +const payInvoice = async(logger, id) => { + const result = await postForm(`/invoices/${id}/pay`); + return JSON.parse(result); +}; + +const payOutstandingInvoicesForCustomer = async(logger, customer_id) => { + let success = true; + const customer = await retrieveCustomer(logger, customer_id); + const {subscriptions} = customer; + logger.debug({subscriptions}, 'payOutstandingInvoicesForCustomer - subscriptions'); + if (subscriptions && subscriptions.data.length > 0) { + const promises = subscriptions.data + .filter((s) => ['incomplete', 'past_due'].includes(s.status) || s.pending_update) + .map((s) => payInvoice(logger, s.latest_invoice)); + const invoices = await Promise.all(promises); + if (invoices.find((i) => 'paid' !== i.status)) { + success = false; + } + } + return success; +}; + +const retrieveSubscription = async(logger, id) => + await getJSON(`/subscriptions/${id}?expand[]=latest_invoice`); + +const cancelSubscription = async(logger, id) => + await deleteJSON(`/subscriptions/${id}`); + +const retrievePaymentMethod = async(logger, id) => await getJSON(`/payment_methods/${id}`); + +const calculateInvoiceAmount = async(logger, products) => { + assert.ok(Array.isArray(products) && products.length, 'calculateInvoiceAmount: products must be array'); + assert.ok(!products.find((p) => !p.priceId || !p.quantity), 'calculateInvoiceAmount: invalid products array'); + + const prices = await Promise.all(products.map((p) => { + return getJSON(`/prices/${p.priceId}?expand[]=tiers`); + })); + logger.debug({prices, products}, 'calculateInvoiceAmount retrieved prices'); + + const total = prices.reduce((acc, pr) => { + const product = products.find((product) => product.priceId === pr.id); + logger.debug({product}, 'calculating price for line item'); + if (pr.billing_scheme === 'per_unit') { + const lineItemCost = pr.unit_amount * product.quantity; + logger.debug(`per-unit pricing: ${product.quantity} * ${pr.unit_amount} = ${lineItemCost} usd`); + return acc + lineItemCost; + } + else if (pr.billing_scheme === 'tiered') { + const tier = pr.tiers.find((t) => product.quantity <= t.up_to || t.up_to === null); + if (typeof tier.flat_amount === 'number') { + const lineItemCost = tier.flat_amount; + logger.debug({tier}, `tiered pricing, flat amount: ${product.quantity} = ${lineItemCost} usd`); + return acc + lineItemCost; + } + else { + const lineItemCost = tier.unit_amount * product.quantity; + logger.debug({tier}, + `tiered pricing, per-unit based: ${product.quantity} * ${tier.unit_amount} = ${lineItemCost} usd`); + return acc + (tier.unit_amount * product.quantity); + } + } + else { + // TODO: handle volume pricing + assert(false, `calculateInvoiceAmount: billing_scheme ${pr.billing_scheme} not implemented!!`); + } + + }, 0); + logger.debug(`calculateInvoiceAmount total cost ${total}`); + return {amount: total, currency: prices[0].currency}; +}; + +const createPaymentIntent = async(logger, + {account_sid, stripe_customer_id, amount, email, currency, stripe_payment_method_id}) => { + const obj = { + amount, + currency, + customer: stripe_customer_id, + payment_method: stripe_payment_method_id, + receipt_email: email, + metadata: { + account_sid + }, + setup_future_usage: 'off_session' + }; + const result = await postForm('/payment_intents', formurlencoded(obj)); + return JSON.parse(result); +}; + +module.exports = { + listProducts, + listPrices, + createCustomer, + retrieveCustomer, + updateCustomer, + deleteCustomer, + createSubscription, + retrieveSubscription, + cancelSubscription, + updateSubscription, + retrievePaymentMethod, + calculateInvoiceAmount, + createPaymentIntent, + attachPaymentMethod, + detachPaymentMethod, + retrieveUpcomingInvoice, + payOutstandingInvoicesForCustomer, + retrieveInvoice, + retrieveProduct, + retrievePricesForProduct +}; diff --git a/package.json b/package.json index 4bca457..0cf5a18 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "jambonz-api-server", - "version": "1.2.1", + "version": "1.2.0", "description": "", "main": "app.js", "scripts": { "start": "node app.js", - "test": "NODE_ENV=test JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_MYSQL_PORT=3360 JAMBONES_REDIS_HOST=localhost JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ | ./node_modules/.bin/tap-spec", + "test": "NODE_ENV=test JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_TIME_SERIES_HOST=127.0.0.1 JAMBONES_LOGLEVEL=error JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/ ", "integration-test": "NODE_ENV=test JAMBONES_TIME_SERIES_HOST=127.0.0.1 AWS_REGION='us-east-1' JAMBONES_CURRENCY=USD JWT_SECRET=foobarbazzle JAMBONES_MYSQL_HOST=127.0.0.1 JAMBONES_MYSQL_PORT=3360 JAMBONES_MYSQL_USER=jambones_test JAMBONES_MYSQL_PASSWORD=jambones_test JAMBONES_MYSQL_DATABASE=jambones_test JAMBONES_REDIS_HOST=localhost JAMBONES_REDIS_PORT=16379 JAMBONES_LOGLEVEL=debug JAMBONES_CREATE_CALL_URL=http://localhost/v1/createCall node test/serve-integration.js", "coverage": "./node_modules/.bin/nyc --reporter html --report-dir ./coverage npm run test", "jslint": "eslint app.js lib" @@ -16,29 +16,40 @@ "url": "https://github.com/jambonz/jambonz-api-server.git" }, "dependencies": { - "@jambonz/db-helpers": "^0.5.5", - "@jambonz/messaging-382com": "0.0.2", - "@jambonz/messaging-peerless": "0.0.9", - "@jambonz/messaging-simwood": "0.0.4", - "@jambonz/realtimedb-helpers": "0.2.19", + "@google-cloud/speech": "^4.2.0", + "@google-cloud/text-to-speech": "^3.1.3", + "@jambonz/db-helpers": "^0.6.12", + "@jambonz/realtimedb-helpers": "^0.4.3", + "@jambonz/time-series": "^0.1.5", + "argon2-ffi": "^2.0.0", + "aws-sdk": "^2.839.0", + "bent": "^7.3.12", "cors": "^2.8.5", + "debug": "^4.3.1", "express": "^4.17.1", + "form-data": "^2.3.3", + "form-urlencoded": "^4.2.1", + "google-libphonenumber": "^3.2.15", + "jsonwebtoken": "^8.5.1", + "mailgun.js": "^3.3.0", "mysql2": "^2.2.5", "passport": "^0.4.1", "passport-http-bearer": "^1.0.1", "pino": "^5.17.0", + "qs": "^6.7.0", "request": "^2.88.2", "request-debug": "^0.2.0", - "swagger-ui-express": "^4.1.5", + "short-uuid": "^4.1.0", + "stripe": "^8.138.0", + "swagger-ui-express": "^4.1.6", "uuid": "^3.4.0", "yamljs": "^0.3.0" }, "devDependencies": { - "blue-tape": "^1.0.0", - "eslint": "^7.15.0", + "eslint": "^7.17.0", "eslint-plugin-promise": "^4.2.1", "nyc": "^15.1.0", "request-promise-native": "^1.0.9", - "tap-spec": "^5.0.0" + "tape": "^5.2.2" } } diff --git a/test.out b/test.out deleted file mode 100644 index 3ccdc8e..0000000 --- a/test.out +++ /dev/null @@ -1,998 +0,0 @@ - -> jambones-api-server@1.0.0 test /Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server -> NODE_ENV=test node test/ | ./node_modules/.bin/tap-spec - - - creating jambones_test database - - ✔ database successfully created - - creating schema - - ✔ schema successfully created - - creating auth token - - ✔ auth token successfully created - - service provider tests - - ✔ fails with 401 if Bearer token not supplied - {"level":50, "time": "2019-12-05T14:33:46.000Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"Error querying for api key","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Handshake.onConnect (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:64:7)\n at Handshake. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Handshake._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Handshake.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Handshake.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - {"level":50, "time": "2019-12-05T14:33:46.002Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"burped error","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Handshake.onConnect (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:64:7)\n at Handshake. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Handshake._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Handshake.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Handshake.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} -{ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - name: 'StatusCodeError', - statusCode: 500, - message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', - error: { msg: 'ER_NO_DB_ERROR: No database selected' }, - options: - { baseUrl: 'http://127.0.0.1:3000/v1', - resolveWithFullResponse: true, - auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, - json: true, - body: { name: 'daveh' }, - uri: '/ServiceProviders', - method: 'POST', - callback: [Function: RP$callback], - transform: undefined, - simple: true, - transform2xxOnly: false }, - response: - IncomingMessage { - _readableState: - ReadableState { - objectMode: false, - highWaterMark: 16384, - buffer: BufferList { head: null, tail: null, length: 0 }, - length: 0, - pipes: null, - pipesCount: 0, - flowing: true, - ended: true, - endEmitted: true, - reading: false, - sync: true, - needReadable: false, - emittedReadable: false, - readableListening: false, - resumeScheduled: false, - paused: false, - emitClose: true, - destroyed: false, - defaultEncoding: 'utf8', - awaitDrain: 0, - readingMore: true, - decoder: null, - encoding: null }, - readable: false, - _events: - [Object: null prototype] { - end: [Array], - close: [Array], - data: [Function], - error: [Function] }, - _eventsCount: 4, - _maxListeners: undefined, - socket: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 109, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - connection: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 109, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - httpVersionMajor: 1, - httpVersionMinor: 1, - httpVersion: '1.1', - complete: true, - headers: - { 'x-powered-by': 'Express', - 'content-type': 'application/json; charset=utf-8', - 'content-length': '46', - etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - date: 'Thu, 05 Dec 2019 14:33:45 GMT', - connection: 'close' }, - rawHeaders: - [ 'X-Powered-By', - 'Express', - 'Content-Type', - 'application/json; charset=utf-8', - 'Content-Length', - '46', - 'ETag', - 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - 'Date', - 'Thu, 05 Dec 2019 14:33:45 GMT', - 'Connection', - 'close' ], - trailers: {}, - rawTrailers: [], - aborted: false, - upgrade: false, - url: '', - method: null, - statusCode: 500, - statusMessage: 'Internal Server Error', - client: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 109, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - _consuming: false, - _dumped: false, - req: - ClientRequest { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - output: [], - outputEncodings: [], - outputCallbacks: [], - outputSize: 0, - writable: true, - _last: true, - chunkedEncoding: false, - shouldKeepAlive: false, - useChunkedEncodingByDefault: true, - sendDate: false, - _removedConnection: false, - _removedContLen: false, - _removedTE: false, - _contentLength: null, - _hasBody: true, - _trailer: '', - finished: true, - _headerSent: true, - socket: [Socket], - connection: [Socket], - _header: - 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', - _onPendingData: [Function: noopPendingOutput], - agent: [Agent], - socketPath: undefined, - timeout: undefined, - method: 'POST', - path: '/v1/ServiceProviders', - _ended: true, - res: [Circular], - aborted: undefined, - timeoutCb: null, - upgradeOrConnect: false, - parser: null, - maxHeadersCount: null, - [Symbol(isCorked)]: false, - [Symbol(outHeadersKey)]: [Object] }, - request: - Request { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - resolveWithFullResponse: true, - body: '{"name":"daveh"}', - uri: [Url], - method: 'POST', - readable: true, - writable: true, - explicitMethod: true, - _qs: [Querystring], - _auth: [Auth], - _oauth: [OAuth], - _multipart: [Multipart], - _redirect: [Redirect], - _tunnel: [Tunnel], - _rp_resolve: [Function], - _rp_reject: [Function], - _rp_promise: [Promise], - _rp_callbackOrig: undefined, - callback: [Function], - _rp_options: [Object], - headers: [Object], - setHeader: [Function], - hasHeader: [Function], - getHeader: [Function], - removeHeader: [Function], - localAddress: undefined, - pool: {}, - dests: [], - __isRequestRequest: true, - _callback: [Function: RP$callback], - proxy: null, - tunnel: false, - setHost: true, - originalCookieHeader: undefined, - _disableCookies: true, - _jar: undefined, - port: '3000', - host: '127.0.0.1', - path: '/v1/ServiceProviders', - _json: true, - httpModule: [Object], - agentClass: [Function], - agent: [Agent], - _started: true, - href: 'http://127.0.0.1:3000/v1/ServiceProviders', - req: [ClientRequest], - ntick: true, - response: [Circular], - originalHost: '127.0.0.1:3000', - originalHostHeaderName: 'host', - responseContent: [Circular], - _destdata: true, - _ended: true, - _callbackCalled: true }, - toJSON: [Function: responseToJSON], - caseless: Caseless { dict: [Object] }, - body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - - ✖ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - ------------------------------------------------------------------------ - operator: error - expected: |- - undefined - actual: |- - { name: 'StatusCodeError', statusCode: 500, message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', error: { msg: 'ER_NO_DB_ERROR: No database selected' }, options: { baseUrl: 'http://127.0.0.1:3000/v1', resolveWithFullResponse: true, auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/ServiceProviders', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, transform2xxOnly: false }, response: { _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: true, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: false, _events: { end: [ [Function: responseOnEnd], [Function] ], close: [ [Function], [Function] ], data: [Function], error: [Function] }, _eventsCount: 4, _maxListeners: undefined, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/ServiceProviders', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/ServiceProviders', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:45 GMT', connection: 'close' }, rawHeaders: [ 'X-Powered-By', 'Express', 'Content-Type', 'application/json; charset=utf-8', 'Content-Length', '46', 'ETag', 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', 'Date', 'Thu, 05 Dec 2019 14:33:45 GMT', 'Connection', 'close' ], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 500, statusMessage: 'Internal Server Error', client: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/ServiceProviders', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, _consuming: false, _dumped: false, req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, _header: 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/ServiceProviders', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, request: { _events: { error: [Function: bound ], complete: [Function: bound ], pipe: [Function], data: [Function], end: [Function] }, _eventsCount: 5, _maxListeners: undefined, resolveWithFullResponse: true, body: '{"name":"daveh"}', uri: { protocol: 'http:', slashes: true, auth: null, host: '127.0.0.1:3000', port: '3000', hostname: '127.0.0.1', hash: null, search: null, query: null, pathname: '/v1/ServiceProviders', path: '/v1/ServiceProviders', href: 'http://127.0.0.1:3000/v1/ServiceProviders' }, method: 'POST', readable: true, writable: true, explicitMethod: true, _qs: { request: [Circular], lib: { formats: [Object], parse: [Function], stringify: [Function] }, useQuerystring: undefined, parseOptions: {}, stringifyOptions: {} }, _auth: { request: [Circular], hasAuth: true, sentAuth: true, bearerToken: '38700987-c7a4-4685-a5bb-af378f9734de', user: null, pass: null }, _oauth: { request: [Circular], params: null }, _multipart: { request: [Circular], boundary: '6f3aeb4b-5874-4dd4-adba-e1cf57f1bf2b', chunked: false, body: null }, _redirect: { request: [Circular], followRedirect: true, followRedirects: true, followAllRedirects: false, followOriginalHttpMethod: false, allowRedirect: [Function], maxRedirects: 10, redirects: [], redirectsFollowed: 0, removeRefererHeader: false }, _tunnel: { request: [Circular], proxyHeaderWhiteList: [ 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'accept-ranges', 'cache-control', 'content-encoding', 'content-language', 'content-location', 'content-md5', 'content-range', 'content-type', 'connection', 'date', 'expect', 'max-forwards', 'pragma', 'referer', 'te', 'user-agent', 'via' ], proxyHeaderExclusiveList: [] }, _rp_resolve: [Function], _rp_reject: [Function], _rp_promise: {}, _rp_callbackOrig: undefined, callback: [Function], _rp_options: { baseUrl: 'http://127.0.0.1:3000/v1', resolveWithFullResponse: true, auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/ServiceProviders', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, transform2xxOnly: false }, headers: { authorization: 'Bearer 38700987-c7a4-4685-a5bb-af378f9734de', accept: 'application/json', 'content-type': 'application/json', 'content-length': 16 }, setHeader: [Function], hasHeader: [Function], getHeader: [Function], removeHeader: [Function], localAddress: undefined, pool: {}, dests: [], __isRequestRequest: true, _callback: [Function: RP$callback], proxy: null, tunnel: false, setHost: true, originalCookieHeader: undefined, _disableCookies: true, _jar: undefined, port: '3000', host: '127.0.0.1', path: '/v1/ServiceProviders', _json: true, httpModule: { _connectionListener: [Function: connectionListener], METHODS: [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ], STATUS_CODES: { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a Teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Unordered Collection', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required' }, Agent: [Function: Agent], ClientRequest: [Function: ClientRequest], globalAgent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, IncomingMessage: [Function: IncomingMessage], OutgoingMessage: [Function: OutgoingMessage], Server: [Function: Server], ServerResponse: [Function: ServerResponse], createServer: [Function: createServer], get: [Function: get], request: [Function: request], maxHeaderSize: 8192 }, agentClass: [Function: Agent], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, _started: true, href: 'http://127.0.0.1:3000/v1/ServiceProviders', req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, connection: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, _header: 'POST /v1/ServiceProviders HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/ServiceProviders', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, ntick: true, response: [Circular], originalHost: '127.0.0.1:3000', originalHostHeaderName: 'host', responseContent: [Circular], _destdata: true, _ended: true, _callbackCalled: true }, toJSON: [Function: responseToJSON], caseless: { dict: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:45 GMT', connection: 'close' } }, body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - at: Test.test (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/test/service-providers.js:76:7) - stack: |- - StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - - - voip carrier tests - - {"level":50, "time": "2019-12-05T14:33:46.032Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"Error querying for api key","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - {"level":50, "time": "2019-12-05T14:33:46.032Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"burped error","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} -{ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - name: 'StatusCodeError', - statusCode: 500, - message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', - error: { msg: 'ER_NO_DB_ERROR: No database selected' }, - options: - { baseUrl: 'http://127.0.0.1:3000/v1', - resolveWithFullResponse: true, - auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, - json: true, - body: { name: 'daveh' }, - uri: '/VoipCarriers', - method: 'POST', - callback: [Function: RP$callback], - transform: undefined, - simple: true, - transform2xxOnly: false }, - response: - IncomingMessage { - _readableState: - ReadableState { - objectMode: false, - highWaterMark: 16384, - buffer: BufferList { head: null, tail: null, length: 0 }, - length: 0, - pipes: null, - pipesCount: 0, - flowing: true, - ended: true, - endEmitted: true, - reading: false, - sync: true, - needReadable: false, - emittedReadable: false, - readableListening: false, - resumeScheduled: false, - paused: false, - emitClose: true, - destroyed: false, - defaultEncoding: 'utf8', - awaitDrain: 0, - readingMore: true, - decoder: null, - encoding: null }, - readable: false, - _events: - [Object: null prototype] { - end: [Array], - close: [Array], - data: [Function], - error: [Function] }, - _eventsCount: 4, - _maxListeners: undefined, - socket: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 180, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - connection: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 180, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - httpVersionMajor: 1, - httpVersionMinor: 1, - httpVersion: '1.1', - complete: true, - headers: - { 'x-powered-by': 'Express', - 'content-type': 'application/json; charset=utf-8', - 'content-length': '46', - etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - date: 'Thu, 05 Dec 2019 14:33:46 GMT', - connection: 'close' }, - rawHeaders: - [ 'X-Powered-By', - 'Express', - 'Content-Type', - 'application/json; charset=utf-8', - 'Content-Length', - '46', - 'ETag', - 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - 'Date', - 'Thu, 05 Dec 2019 14:33:46 GMT', - 'Connection', - 'close' ], - trailers: {}, - rawTrailers: [], - aborted: false, - upgrade: false, - url: '', - method: null, - statusCode: 500, - statusMessage: 'Internal Server Error', - client: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 180, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - _consuming: false, - _dumped: false, - req: - ClientRequest { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - output: [], - outputEncodings: [], - outputCallbacks: [], - outputSize: 0, - writable: true, - _last: true, - chunkedEncoding: false, - shouldKeepAlive: false, - useChunkedEncodingByDefault: true, - sendDate: false, - _removedConnection: false, - _removedContLen: false, - _removedTE: false, - _contentLength: null, - _hasBody: true, - _trailer: '', - finished: true, - _headerSent: true, - socket: [Socket], - connection: [Socket], - _header: - 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', - _onPendingData: [Function: noopPendingOutput], - agent: [Agent], - socketPath: undefined, - timeout: undefined, - method: 'POST', - path: '/v1/VoipCarriers', - _ended: true, - res: [Circular], - aborted: undefined, - timeoutCb: null, - upgradeOrConnect: false, - parser: null, - maxHeadersCount: null, - [Symbol(isCorked)]: false, - [Symbol(outHeadersKey)]: [Object] }, - request: - Request { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - resolveWithFullResponse: true, - body: '{"name":"daveh"}', - uri: [Url], - method: 'POST', - readable: true, - writable: true, - explicitMethod: true, - _qs: [Querystring], - _auth: [Auth], - _oauth: [OAuth], - _multipart: [Multipart], - _redirect: [Redirect], - _tunnel: [Tunnel], - _rp_resolve: [Function], - _rp_reject: [Function], - _rp_promise: [Promise], - _rp_callbackOrig: undefined, - callback: [Function], - _rp_options: [Object], - headers: [Object], - setHeader: [Function], - hasHeader: [Function], - getHeader: [Function], - removeHeader: [Function], - localAddress: undefined, - pool: {}, - dests: [], - __isRequestRequest: true, - _callback: [Function: RP$callback], - proxy: null, - tunnel: false, - setHost: true, - originalCookieHeader: undefined, - _disableCookies: true, - _jar: undefined, - port: '3000', - host: '127.0.0.1', - path: '/v1/VoipCarriers', - _json: true, - httpModule: [Object], - agentClass: [Function], - agent: [Agent], - _started: true, - href: 'http://127.0.0.1:3000/v1/VoipCarriers', - req: [ClientRequest], - ntick: true, - response: [Circular], - originalHost: '127.0.0.1:3000', - originalHostHeaderName: 'host', - responseContent: [Circular], - _destdata: true, - _ended: true, - _callbackCalled: true }, - toJSON: [Function: responseToJSON], - caseless: Caseless { dict: [Object] }, - body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - - ✖ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - ------------------------------------------------------------------------ - operator: error - expected: |- - undefined - actual: |- - { name: 'StatusCodeError', statusCode: 500, message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', error: { msg: 'ER_NO_DB_ERROR: No database selected' }, options: { baseUrl: 'http://127.0.0.1:3000/v1', resolveWithFullResponse: true, auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, transform2xxOnly: false }, response: { _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: true, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: false, _events: { end: [ [Function: responseOnEnd], [Function] ], close: [ [Function], [Function] ], data: [Function], error: [Function] }, _eventsCount: 4, _maxListeners: undefined, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' }, rawHeaders: [ 'X-Powered-By', 'Express', 'Content-Type', 'application/json; charset=utf-8', 'Content-Length', '46', 'ETag', 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', 'Date', 'Thu, 05 Dec 2019 14:33:46 GMT', 'Connection', 'close' ], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 500, statusMessage: 'Internal Server Error', client: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, _consuming: false, _dumped: false, req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, request: { _events: { error: [Function: bound ], complete: [Function: bound ], pipe: [Function], data: [Function], end: [Function] }, _eventsCount: 5, _maxListeners: undefined, resolveWithFullResponse: true, body: '{"name":"daveh"}', uri: { protocol: 'http:', slashes: true, auth: null, host: '127.0.0.1:3000', port: '3000', hostname: '127.0.0.1', hash: null, search: null, query: null, pathname: '/v1/VoipCarriers', path: '/v1/VoipCarriers', href: 'http://127.0.0.1:3000/v1/VoipCarriers' }, method: 'POST', readable: true, writable: true, explicitMethod: true, _qs: { request: [Circular], lib: { formats: [Object], parse: [Function], stringify: [Function] }, useQuerystring: undefined, parseOptions: {}, stringifyOptions: {} }, _auth: { request: [Circular], hasAuth: true, sentAuth: true, bearerToken: '38700987-c7a4-4685-a5bb-af378f9734de', user: null, pass: null }, _oauth: { request: [Circular], params: null }, _multipart: { request: [Circular], boundary: 'e0934a63-e174-408d-8db4-cb93ec98a99c', chunked: false, body: null }, _redirect: { request: [Circular], followRedirect: true, followRedirects: true, followAllRedirects: false, followOriginalHttpMethod: false, allowRedirect: [Function], maxRedirects: 10, redirects: [], redirectsFollowed: 0, removeRefererHeader: false }, _tunnel: { request: [Circular], proxyHeaderWhiteList: [ 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'accept-ranges', 'cache-control', 'content-encoding', 'content-language', 'content-location', 'content-md5', 'content-range', 'content-type', 'connection', 'date', 'expect', 'max-forwards', 'pragma', 'referer', 'te', 'user-agent', 'via' ], proxyHeaderExclusiveList: [] }, _rp_resolve: [Function], _rp_reject: [Function], _rp_promise: {}, _rp_callbackOrig: undefined, callback: [Function], _rp_options: { baseUrl: 'http://127.0.0.1:3000/v1', resolveWithFullResponse: true, auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, transform2xxOnly: false }, headers: { authorization: 'Bearer 38700987-c7a4-4685-a5bb-af378f9734de', accept: 'application/json', 'content-type': 'application/json', 'content-length': 16 }, setHeader: [Function], hasHeader: [Function], getHeader: [Function], removeHeader: [Function], localAddress: undefined, pool: {}, dests: [], __isRequestRequest: true, _callback: [Function: RP$callback], proxy: null, tunnel: false, setHost: true, originalCookieHeader: undefined, _disableCookies: true, _jar: undefined, port: '3000', host: '127.0.0.1', path: '/v1/VoipCarriers', _json: true, httpModule: { _connectionListener: [Function: connectionListener], METHODS: [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ], STATUS_CODES: { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a Teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Unordered Collection', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required' }, Agent: [Function: Agent], ClientRequest: [Function: ClientRequest], globalAgent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, IncomingMessage: [Function: IncomingMessage], OutgoingMessage: [Function: OutgoingMessage], Server: [Function: Server], ServerResponse: [Function: ServerResponse], createServer: [Function: createServer], get: [Function: get], request: [Function: request], maxHeaderSize: 8192 }, agentClass: [Function: Agent], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, _started: true, href: 'http://127.0.0.1:3000/v1/VoipCarriers', req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, connection: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, ntick: true, response: [Circular], originalHost: '127.0.0.1:3000', originalHostHeaderName: 'host', responseContent: [Circular], _destdata: true, _ended: true, _callbackCalled: true }, toJSON: [Function: responseToJSON], caseless: { dict: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' } }, body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - at: Test.test (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/test/voip-carriers.js:106:7) - stack: |- - StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - - - account tests - - {"level":50, "time": "2019-12-05T14:33:46.048Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"Error querying for api key","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - {"level":50, "time": "2019-12-05T14:33:46.048Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"burped error","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - - ✖ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - ------------------------------------------------------------------------ - operator: error - expected: |- - undefined - actual: |- - { name: 'StatusCodeError', statusCode: 500, message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', error: { msg: 'ER_NO_DB_ERROR: No database selected' }, options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, response: { _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: true, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: false, _events: { end: [ [Function: responseOnEnd], [Function] ], close: [ [Function], [Function] ], data: [Function], error: [Function] }, _eventsCount: 4, _maxListeners: undefined, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' }, rawHeaders: [ 'X-Powered-By', 'Express', 'Content-Type', 'application/json; charset=utf-8', 'Content-Length', '46', 'ETag', 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', 'Date', 'Thu, 05 Dec 2019 14:33:46 GMT', 'Connection', 'close' ], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 500, statusMessage: 'Internal Server Error', client: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, _consuming: false, _dumped: false, req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, request: { _events: { error: [Function: bound ], complete: [Function: bound ], pipe: [Function], data: [Function], end: [Function] }, _eventsCount: 5, _maxListeners: undefined, body: '{"name":"daveh"}', uri: { protocol: 'http:', slashes: true, auth: null, host: '127.0.0.1:3000', port: '3000', hostname: '127.0.0.1', hash: null, search: null, query: null, pathname: '/v1/VoipCarriers', path: '/v1/VoipCarriers', href: 'http://127.0.0.1:3000/v1/VoipCarriers' }, method: 'POST', readable: true, writable: true, explicitMethod: true, _qs: { request: [Circular], lib: { formats: [Object], parse: [Function], stringify: [Function] }, useQuerystring: undefined, parseOptions: {}, stringifyOptions: {} }, _auth: { request: [Circular], hasAuth: true, sentAuth: true, bearerToken: '38700987-c7a4-4685-a5bb-af378f9734de', user: null, pass: null }, _oauth: { request: [Circular], params: null }, _multipart: { request: [Circular], boundary: '74402418-46db-4128-9e3a-e1db8826132f', chunked: false, body: null }, _redirect: { request: [Circular], followRedirect: true, followRedirects: true, followAllRedirects: false, followOriginalHttpMethod: false, allowRedirect: [Function], maxRedirects: 10, redirects: [], redirectsFollowed: 0, removeRefererHeader: false }, _tunnel: { request: [Circular], proxyHeaderWhiteList: [ 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'accept-ranges', 'cache-control', 'content-encoding', 'content-language', 'content-location', 'content-md5', 'content-range', 'content-type', 'connection', 'date', 'expect', 'max-forwards', 'pragma', 'referer', 'te', 'user-agent', 'via' ], proxyHeaderExclusiveList: [] }, _rp_resolve: [Function], _rp_reject: [Function], _rp_promise: {}, _rp_callbackOrig: undefined, callback: [Function], _rp_options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, headers: { authorization: 'Bearer 38700987-c7a4-4685-a5bb-af378f9734de', accept: 'application/json', 'content-type': 'application/json', 'content-length': 16 }, setHeader: [Function], hasHeader: [Function], getHeader: [Function], removeHeader: [Function], localAddress: undefined, pool: {}, dests: [], __isRequestRequest: true, _callback: [Function: RP$callback], proxy: null, tunnel: false, setHost: true, originalCookieHeader: undefined, _disableCookies: true, _jar: undefined, port: '3000', host: '127.0.0.1', path: '/v1/VoipCarriers', _json: true, httpModule: { _connectionListener: [Function: connectionListener], METHODS: [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ], STATUS_CODES: { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a Teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Unordered Collection', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required' }, Agent: [Function: Agent], ClientRequest: [Function: ClientRequest], globalAgent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, IncomingMessage: [Function: IncomingMessage], OutgoingMessage: [Function: OutgoingMessage], Server: [Function: Server], ServerResponse: [Function: ServerResponse], createServer: [Function: createServer], get: [Function: get], request: [Function: request], maxHeaderSize: 8192 }, agentClass: [Function: Agent], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, _started: true, href: 'http://127.0.0.1:3000/v1/VoipCarriers', req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, connection: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, ntick: true, response: [Circular], originalHost: '127.0.0.1:3000', originalHostHeaderName: 'host', responseContent: [Circular], _destdata: true, _ended: true, _callbackCalled: true }, toJSON: [Function: responseToJSON], caseless: { dict: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' } }, body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - at: Test.test (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/test/accounts.js:96:7) - stack: |- - StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - - - phone number tests - - {"level":50, "time": "2019-12-05T14:33:46.062Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"Error querying for api key","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - {"level":50, "time": "2019-12-05T14:33:46.062Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"burped error","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} -{ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - name: 'StatusCodeError', - statusCode: 500, - message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', - error: { msg: 'ER_NO_DB_ERROR: No database selected' }, - options: - { baseUrl: 'http://127.0.0.1:3000/v1', - auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, - json: true, - body: { name: 'daveh' }, - uri: '/VoipCarriers', - method: 'POST', - callback: [Function: RP$callback], - transform: undefined, - simple: true, - resolveWithFullResponse: false, - transform2xxOnly: false }, - response: - IncomingMessage { - _readableState: - ReadableState { - objectMode: false, - highWaterMark: 16384, - buffer: BufferList { head: null, tail: null, length: 0 }, - length: 0, - pipes: null, - pipesCount: 0, - flowing: true, - ended: true, - endEmitted: true, - reading: false, - sync: true, - needReadable: false, - emittedReadable: false, - readableListening: false, - resumeScheduled: false, - paused: false, - emitClose: true, - destroyed: false, - defaultEncoding: 'utf8', - awaitDrain: 0, - readingMore: true, - decoder: null, - encoding: null }, - readable: false, - _events: - [Object: null prototype] { - end: [Array], - close: [Array], - data: [Function], - error: [Function] }, - _eventsCount: 4, - _maxListeners: undefined, - socket: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 303, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - connection: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 303, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - httpVersionMajor: 1, - httpVersionMinor: 1, - httpVersion: '1.1', - complete: true, - headers: - { 'x-powered-by': 'Express', - 'content-type': 'application/json; charset=utf-8', - 'content-length': '46', - etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - date: 'Thu, 05 Dec 2019 14:33:46 GMT', - connection: 'close' }, - rawHeaders: - [ 'X-Powered-By', - 'Express', - 'Content-Type', - 'application/json; charset=utf-8', - 'Content-Length', - '46', - 'ETag', - 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', - 'Date', - 'Thu, 05 Dec 2019 14:33:46 GMT', - 'Connection', - 'close' ], - trailers: {}, - rawTrailers: [], - aborted: false, - upgrade: false, - url: '', - method: null, - statusCode: 500, - statusMessage: 'Internal Server Error', - client: - Socket { - connecting: false, - _hadError: false, - _handle: [TCP], - _parent: null, - _host: null, - _readableState: [ReadableState], - readable: true, - _events: [Object], - _eventsCount: 7, - _maxListeners: undefined, - _writableState: [WritableState], - writable: false, - allowHalfOpen: false, - _sockname: null, - _pendingData: null, - _pendingEncoding: '', - server: null, - _server: null, - parser: null, - _httpMessage: [ClientRequest], - [Symbol(asyncId)]: 303, - [Symbol(lastWriteQueueSize)]: 0, - [Symbol(timeout)]: null, - [Symbol(kBytesRead)]: 0, - [Symbol(kBytesWritten)]: 0 }, - _consuming: false, - _dumped: false, - req: - ClientRequest { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - output: [], - outputEncodings: [], - outputCallbacks: [], - outputSize: 0, - writable: true, - _last: true, - chunkedEncoding: false, - shouldKeepAlive: false, - useChunkedEncodingByDefault: true, - sendDate: false, - _removedConnection: false, - _removedContLen: false, - _removedTE: false, - _contentLength: null, - _hasBody: true, - _trailer: '', - finished: true, - _headerSent: true, - socket: [Socket], - connection: [Socket], - _header: - 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', - _onPendingData: [Function: noopPendingOutput], - agent: [Agent], - socketPath: undefined, - timeout: undefined, - method: 'POST', - path: '/v1/VoipCarriers', - _ended: true, - res: [Circular], - aborted: undefined, - timeoutCb: null, - upgradeOrConnect: false, - parser: null, - maxHeadersCount: null, - [Symbol(isCorked)]: false, - [Symbol(outHeadersKey)]: [Object] }, - request: - Request { - _events: [Object], - _eventsCount: 5, - _maxListeners: undefined, - body: '{"name":"daveh"}', - uri: [Url], - method: 'POST', - readable: true, - writable: true, - explicitMethod: true, - _qs: [Querystring], - _auth: [Auth], - _oauth: [OAuth], - _multipart: [Multipart], - _redirect: [Redirect], - _tunnel: [Tunnel], - _rp_resolve: [Function], - _rp_reject: [Function], - _rp_promise: [Promise], - _rp_callbackOrig: undefined, - callback: [Function], - _rp_options: [Object], - headers: [Object], - setHeader: [Function], - hasHeader: [Function], - getHeader: [Function], - removeHeader: [Function], - localAddress: undefined, - pool: {}, - dests: [], - __isRequestRequest: true, - _callback: [Function: RP$callback], - proxy: null, - tunnel: false, - setHost: true, - originalCookieHeader: undefined, - _disableCookies: true, - _jar: undefined, - port: '3000', - host: '127.0.0.1', - path: '/v1/VoipCarriers', - _json: true, - httpModule: [Object], - agentClass: [Function], - agent: [Agent], - _started: true, - href: 'http://127.0.0.1:3000/v1/VoipCarriers', - req: [ClientRequest], - ntick: true, - response: [Circular], - originalHost: '127.0.0.1:3000', - originalHostHeaderName: 'host', - responseContent: [Circular], - _destdata: true, - _ended: true, - _callbackCalled: true }, - toJSON: [Function: responseToJSON], - caseless: Caseless { dict: [Object] }, - body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - - ✖ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - ------------------------------------------------------------------------ - operator: error - expected: |- - undefined - actual: |- - { name: 'StatusCodeError', statusCode: 500, message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', error: { msg: 'ER_NO_DB_ERROR: No database selected' }, options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, response: { _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: true, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: false, _events: { end: [ [Function: responseOnEnd], [Function] ], close: [ [Function], [Function] ], data: [Function], error: [Function] }, _eventsCount: 4, _maxListeners: undefined, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' }, rawHeaders: [ 'X-Powered-By', 'Express', 'Content-Type', 'application/json; charset=utf-8', 'Content-Length', '46', 'ETag', 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', 'Date', 'Thu, 05 Dec 2019 14:33:46 GMT', 'Connection', 'close' ], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 500, statusMessage: 'Internal Server Error', client: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, _consuming: false, _dumped: false, req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, request: { _events: { error: [Function: bound ], complete: [Function: bound ], pipe: [Function], data: [Function], end: [Function] }, _eventsCount: 5, _maxListeners: undefined, body: '{"name":"daveh"}', uri: { protocol: 'http:', slashes: true, auth: null, host: '127.0.0.1:3000', port: '3000', hostname: '127.0.0.1', hash: null, search: null, query: null, pathname: '/v1/VoipCarriers', path: '/v1/VoipCarriers', href: 'http://127.0.0.1:3000/v1/VoipCarriers' }, method: 'POST', readable: true, writable: true, explicitMethod: true, _qs: { request: [Circular], lib: { formats: [Object], parse: [Function], stringify: [Function] }, useQuerystring: undefined, parseOptions: {}, stringifyOptions: {} }, _auth: { request: [Circular], hasAuth: true, sentAuth: true, bearerToken: '38700987-c7a4-4685-a5bb-af378f9734de', user: null, pass: null }, _oauth: { request: [Circular], params: null }, _multipart: { request: [Circular], boundary: 'a1342398-8a8f-4a35-b8fe-226ebed9666d', chunked: false, body: null }, _redirect: { request: [Circular], followRedirect: true, followRedirects: true, followAllRedirects: false, followOriginalHttpMethod: false, allowRedirect: [Function], maxRedirects: 10, redirects: [], redirectsFollowed: 0, removeRefererHeader: false }, _tunnel: { request: [Circular], proxyHeaderWhiteList: [ 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'accept-ranges', 'cache-control', 'content-encoding', 'content-language', 'content-location', 'content-md5', 'content-range', 'content-type', 'connection', 'date', 'expect', 'max-forwards', 'pragma', 'referer', 'te', 'user-agent', 'via' ], proxyHeaderExclusiveList: [] }, _rp_resolve: [Function], _rp_reject: [Function], _rp_promise: {}, _rp_callbackOrig: undefined, callback: [Function], _rp_options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, headers: { authorization: 'Bearer 38700987-c7a4-4685-a5bb-af378f9734de', accept: 'application/json', 'content-type': 'application/json', 'content-length': 16 }, setHeader: [Function], hasHeader: [Function], getHeader: [Function], removeHeader: [Function], localAddress: undefined, pool: {}, dests: [], __isRequestRequest: true, _callback: [Function: RP$callback], proxy: null, tunnel: false, setHost: true, originalCookieHeader: undefined, _disableCookies: true, _jar: undefined, port: '3000', host: '127.0.0.1', path: '/v1/VoipCarriers', _json: true, httpModule: { _connectionListener: [Function: connectionListener], METHODS: [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ], STATUS_CODES: { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a Teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Unordered Collection', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required' }, Agent: [Function: Agent], ClientRequest: [Function: ClientRequest], globalAgent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, IncomingMessage: [Function: IncomingMessage], OutgoingMessage: [Function: OutgoingMessage], Server: [Function: Server], ServerResponse: [Function: ServerResponse], createServer: [Function: createServer], get: [Function: get], request: [Function: request], maxHeaderSize: 8192 }, agentClass: [Function: Agent], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, _started: true, href: 'http://127.0.0.1:3000/v1/VoipCarriers', req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, connection: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, ntick: true, response: [Circular], originalHost: '127.0.0.1:3000', originalHostHeaderName: 'host', responseContent: [Circular], _destdata: true, _ended: true, _callbackCalled: true }, toJSON: [Function: responseToJSON], caseless: { dict: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' } }, body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - at: Test.test (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/test/phone-numbers.js:118:7) - stack: |- - StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - - - application tests - - {"level":50, "time": "2019-12-05T14:33:46.073Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"Error querying for api key","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - {"level":50, "time": "2019-12-05T14:33:46.073Z","pid":85088,"hostname":"MacBook-Pro-4.local","code":"ER_NO_DB_ERROR","errno":1046,"sqlMessage":"No database selected","sqlState":"3D000","index":0,"sql":"\n SELECT *\n FROM api_keys\n WHERE api_keys.token = '38700987-c7a4-4685-a5bb-af378f9734de'","msg":"burped error","stack":"Error: ER_NO_DB_ERROR: No database selected\n at Query.Sequence._packetToError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:47:14)\n at Query.ErrorPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Query.js:77:18)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)\n at Parser.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:43:10)\n at Protocol.write (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:38:16)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:91:28)\n at Socket. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Socket.emit (events.js:189:13)\n at addChunk (_stream_readable.js:284:12)\n --------------------\n at Protocol._enqueue (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:144:48)\n at PoolConnection.query (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:201:25)\n at getMysqlConnection (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/lib/auth/index.js:17:14)\n at Ping.onOperationComplete (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Pool.js:110:5)\n at Ping. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:525:10)\n at Ping._callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/Connection.js:491:16)\n at Ping.Sequence.end (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:83:24)\n at Ping.Sequence.OkPacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/sequences/Sequence.js:92:8)\n at Protocol._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Protocol.js:291:23)\n at Parser._parsePacket (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/mysql/lib/protocol/Parser.js:433:10)","type":"Error","v":1} - - ✖ StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - ------------------------------------------------------------------------ - operator: error - expected: |- - undefined - actual: |- - { name: 'StatusCodeError', statusCode: 500, message: '500 - {"msg":"ER_NO_DB_ERROR: No database selected"}', error: { msg: 'ER_NO_DB_ERROR: No database selected' }, options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, response: { _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: true, endEmitted: true, reading: false, sync: true, needReadable: false, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: true, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: true, decoder: null, encoding: null }, readable: false, _events: { end: [ [Function: responseOnEnd], [Function] ], close: [ [Function], [Function] ], data: [Function], error: [Function] }, _eventsCount: 4, _maxListeners: undefined, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' }, rawHeaders: [ 'X-Powered-By', 'Express', 'Content-Type', 'application/json; charset=utf-8', 'Content-Length', '46', 'ETag', 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', 'Date', 'Thu, 05 Dec 2019 14:33:46 GMT', 'Connection', 'close' ], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 500, statusMessage: 'Internal Server Error', client: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: BufferList { head: null, tail: null, length: 0 }, length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [ [Function: onClose], [Function: socketCloseListener] ], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: { next: [Object], entry: null, finish: [Function: bound onCorkedFinish] } }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [Circular], connection: [Circular], _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null } }, _consuming: false, _dumped: false, req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, connection: { connecting: false, _hadError: false, _handle: { reading: true, onread: [Function: onStreamRead], onconnection: null }, _parent: null, _host: null, _readableState: { objectMode: false, highWaterMark: 16384, buffer: [Object], length: 0, pipes: null, pipesCount: 0, flowing: true, ended: false, endEmitted: false, reading: true, sync: false, needReadable: true, emittedReadable: false, readableListening: false, resumeScheduled: false, paused: false, emitClose: false, destroyed: false, defaultEncoding: 'utf8', awaitDrain: 0, readingMore: false, decoder: null, encoding: null }, readable: true, _events: { end: [Function: onReadableStreamEnd], free: [Function: onFree], close: [Object], agentRemove: [Function: onRemove], drain: [Function: ondrain], error: [Function: socketErrorListener], finish: [Function: bound onceWrapper] }, _eventsCount: 7, _maxListeners: undefined, _writableState: { objectMode: false, highWaterMark: 16384, finalCalled: true, needDrain: false, ending: true, ended: true, finished: false, destroyed: false, decodeStrings: false, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: false, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, bufferedRequest: null, lastBufferedRequest: null, pendingcb: 1, prefinished: false, errorEmitted: false, emitClose: false, bufferedRequestCount: 0, corkedRequestsFree: [Object] }, writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Circular] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, request: { _events: { error: [Function: bound ], complete: [Function: bound ], pipe: [Function], data: [Function], end: [Function] }, _eventsCount: 5, _maxListeners: undefined, body: '{"name":"daveh"}', uri: { protocol: 'http:', slashes: true, auth: null, host: '127.0.0.1:3000', port: '3000', hostname: '127.0.0.1', hash: null, search: null, query: null, pathname: '/v1/VoipCarriers', path: '/v1/VoipCarriers', href: 'http://127.0.0.1:3000/v1/VoipCarriers' }, method: 'POST', readable: true, writable: true, explicitMethod: true, _qs: { request: [Circular], lib: { formats: [Object], parse: [Function], stringify: [Function] }, useQuerystring: undefined, parseOptions: {}, stringifyOptions: {} }, _auth: { request: [Circular], hasAuth: true, sentAuth: true, bearerToken: '38700987-c7a4-4685-a5bb-af378f9734de', user: null, pass: null }, _oauth: { request: [Circular], params: null }, _multipart: { request: [Circular], boundary: 'fdc0db5c-3755-4cbb-82d7-fa033fa951da', chunked: false, body: null }, _redirect: { request: [Circular], followRedirect: true, followRedirects: true, followAllRedirects: false, followOriginalHttpMethod: false, allowRedirect: [Function], maxRedirects: 10, redirects: [], redirectsFollowed: 0, removeRefererHeader: false }, _tunnel: { request: [Circular], proxyHeaderWhiteList: [ 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'accept-ranges', 'cache-control', 'content-encoding', 'content-language', 'content-location', 'content-md5', 'content-range', 'content-type', 'connection', 'date', 'expect', 'max-forwards', 'pragma', 'referer', 'te', 'user-agent', 'via' ], proxyHeaderExclusiveList: [] }, _rp_resolve: [Function], _rp_reject: [Function], _rp_promise: {}, _rp_callbackOrig: undefined, callback: [Function], _rp_options: { baseUrl: 'http://127.0.0.1:3000/v1', auth: { bearer: '38700987-c7a4-4685-a5bb-af378f9734de' }, json: true, body: { name: 'daveh' }, uri: '/VoipCarriers', method: 'POST', callback: [Function: RP$callback], transform: undefined, simple: true, resolveWithFullResponse: false, transform2xxOnly: false }, headers: { authorization: 'Bearer 38700987-c7a4-4685-a5bb-af378f9734de', accept: 'application/json', 'content-type': 'application/json', 'content-length': 16 }, setHeader: [Function], hasHeader: [Function], getHeader: [Function], removeHeader: [Function], localAddress: undefined, pool: {}, dests: [], __isRequestRequest: true, _callback: [Function: RP$callback], proxy: null, tunnel: false, setHost: true, originalCookieHeader: undefined, _disableCookies: true, _jar: undefined, port: '3000', host: '127.0.0.1', path: '/v1/VoipCarriers', _json: true, httpModule: { _connectionListener: [Function: connectionListener], METHODS: [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SOURCE', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ], STATUS_CODES: { 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a Teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Unordered Collection', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required' }, Agent: [Function: Agent], ClientRequest: [Function: ClientRequest], globalAgent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, IncomingMessage: [Function: IncomingMessage], OutgoingMessage: [Function: OutgoingMessage], Server: [Function: Server], ServerResponse: [Function: ServerResponse], createServer: [Function: createServer], get: [Function: get], request: [Function: request], maxHeaderSize: 8192 }, agentClass: [Function: Agent], agent: { _events: { free: [Function] }, _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: { path: null }, requests: {}, sockets: { '127.0.0.1:3000:': [Object] }, freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, _started: true, href: 'http://127.0.0.1:3000/v1/VoipCarriers', req: { _events: { socket: [Function], response: [Function: bound ], error: [Function: bound ], drain: [Function], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 5, _maxListeners: undefined, output: [], outputEncodings: [], outputCallbacks: [], outputSize: 0, writable: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, useChunkedEncodingByDefault: true, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, connection: { connecting: false, _hadError: false, _handle: [Object], _parent: null, _host: null, _readableState: [Object], readable: true, _events: [Object], _eventsCount: 7, _maxListeners: undefined, _writableState: [Object], writable: false, allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: null, _server: null, parser: null, _httpMessage: [Object] }, _header: 'POST /v1/VoipCarriers HTTP/1.1\r\nhost: 127.0.0.1:3000\r\nauthorization: Bearer 38700987-c7a4-4685-a5bb-af378f9734de\r\naccept: application/json\r\ncontent-type: application/json\r\ncontent-length: 16\r\nConnection: close\r\n\r\n', _onPendingData: [Function: noopPendingOutput], agent: { _events: [Object], _eventsCount: 1, _maxListeners: undefined, defaultPort: 80, protocol: 'http:', options: [Object], requests: [Object], sockets: [Object], freeSockets: [Object], keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256 }, socketPath: undefined, timeout: undefined, method: 'POST', path: '/v1/VoipCarriers', _ended: true, res: [Circular], aborted: undefined, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null }, ntick: true, response: [Circular], originalHost: '127.0.0.1:3000', originalHostHeaderName: 'host', responseContent: [Circular], _destdata: true, _ended: true, _callbackCalled: true }, toJSON: [Function: responseToJSON], caseless: { dict: { 'x-powered-by': 'Express', 'content-type': 'application/json; charset=utf-8', 'content-length': '46', etag: 'W/"2e-aOVpxzvH5QCcOOYpNSVGq9uYJeA"', date: 'Thu, 05 Dec 2019 14:33:46 GMT', connection: 'close' } }, body: { msg: 'ER_NO_DB_ERROR: No database selected' } } } - at: Test.test (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/test/applications.js:106:7) - stack: |- - StatusCodeError: 500 - {"msg":"ER_NO_DB_ERROR: No database selected"} - at new StatusCodeError (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/errors.js:32:15) - at Request.plumbing.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:104:33) - at Request.RP$callback [as _callback] (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request-promise-core/lib/plumbing.js:46:31) - at Request.self.callback (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:185:22) - at Request.emit (events.js:189:13) - at Request. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1161:10) - at Request.emit (events.js:189:13) - at IncomingMessage. (/Users/dhorton/beachdog-enterprises/beachdog-networks/git/jambones.org/git/jambones-api-server/node_modules/request/request.js:1083:12) - at Object.onceWrapper (events.js:277:13) - at IncomingMessage.emit (events.js:194:15) - - - dropping jambones_test database - - ✔ database successfully dropped diff --git a/test/accounts.js b/test/accounts.js index 5084bdc..aa95e57 100644 --- a/test/accounts.js +++ b/test/accounts.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -16,6 +16,7 @@ process.on('unhandledRejection', (reason, p) => { test('account tests', async(t) => { const app = require('../app'); + const logger = app.locals.logger; let sid; try { let result; @@ -25,6 +26,61 @@ test('account tests', async(t) => { const service_provider_sid = await createServiceProvider(request); const phone_number_sid = await createPhoneNumber(request, voip_carrier_sid); + /* add invite codes */ + result = await request.post('/BetaInviteCodes', { + resolveWithFullResponse: true, + json: true, + auth: authAdmin, + body: { + count: 2 + } + }); + t.ok(result.statusCode === 200 && 2 === parseInt(result.body.added), 'successfully added 2 beta codes'); + //console.log(result.body.codes); + + /* claim an invite code */ + /* + const mycodes = result.body.codes; + result = await request.post('/InviteCodes', { + resolveWithFullResponse: true, + json: true, + auth: authAdmin, + body: { + test: true, + code: mycodes[0] + } + }); + t.ok(result.statusCode === 204, 'successfully tested a beta codes'); + result = await request.post('/InviteCodes', { + resolveWithFullResponse: true, + json: true, + auth: authAdmin, + body: { + code: mycodes[0] + } + }); + t.ok(result.statusCode === 204, 'successfully claimed a beta codes'); + */ + + result = await request.post('/BetaInviteCodes', { + resolveWithFullResponse: true, + json: true, + auth: authAdmin, + body: { + count: 50 + } + }); + t.ok(result.statusCode === 200 && 50 === parseInt(result.body.added), 'successfully added 50 beta codes'); + + result = await request.post('/BetaInviteCodes', { + resolveWithFullResponse: true, + json: true, + auth: authAdmin, + body: { + } + }); + t.ok(result.statusCode === 200 && 1 === parseInt(result.body.added), 'successfully added 1 beta codes'); + /* add an account */ result = await request.post('/Accounts', { resolveWithFullResponse: true, @@ -36,12 +92,22 @@ test('account tests', async(t) => { registration_hook: { url: 'http://example.com/reg', method: 'get' - } + }, + webhook_secret: 'foobar' } }); t.ok(result.statusCode === 201, 'successfully created account'); const sid = result.body.sid; + /* query accounts for service providers */ + result = await request.get(`/ServiceProviders/${service_provider_sid}/Accounts`, { + auth: authAdmin, + json: true, + resolveWithFullResponse: true, + }); + //console.log(result.body); + t.ok(result.statusCode === 200, 'successfully queried accounts for service provider'); + /* add an account level api key */ result = await request.post(`/ApiKeys`, { auth: authAdmin, @@ -118,23 +184,12 @@ test('account tests', async(t) => { }); t.ok(result.statusCode === 204, 'successfully assigned phone number to account'); - /* cannot delete account that has phone numbers assigned */ - result = await request.delete(`/Accounts/${sid}`, { - auth: authAdmin, - resolveWithFullResponse: true, - simple: false, - json: true - }); - t.ok(result.statusCode === 422 && result.body.msg === 'cannot delete account with phone numbers', 'cannot delete account with phone numbers'); - /* delete account */ - await request.delete(`ApiKeys/${apiKeySid}`, {auth: {bearer: accountLevelToken}}); - await request.delete(`/PhoneNumbers/${phone_number_sid}`, {auth: authAdmin}); result = await request.delete(`/Accounts/${sid}`, { auth: authAdmin, resolveWithFullResponse: true, }); - t.ok(result.statusCode === 204, 'successfully deleted account after removing phone number'); + t.ok(result.statusCode === 204, 'successfully deleted account'); await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid); await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid); diff --git a/test/applications.js b/test/applications.js index 5dc44a2..5ede901 100644 --- a/test/applications.js +++ b/test/applications.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ diff --git a/test/auth.js b/test/auth.js index d5967cb..756cee8 100644 --- a/test/auth.js +++ b/test/auth.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -56,6 +56,7 @@ test('authentication tests', async(t) => { json: true, body: { name: 'accountA1', + webhook_secret: 'foobar', registration_hook: { url: 'http://example.com' } @@ -69,6 +70,7 @@ test('authentication tests', async(t) => { simple: false, json: true, body: { + webhook_secret: 'foobar', name: 'accountA2' } }); @@ -80,6 +82,7 @@ test('authentication tests', async(t) => { simple: false, json: true, body: { + webhook_secret: 'foobar', name: 'accountB1' } }); @@ -91,6 +94,7 @@ test('authentication tests', async(t) => { simple: false, json: true, body: { + webhook_secret: 'foobar', name: 'accountB2' } }); @@ -170,6 +174,7 @@ test('authentication tests', async(t) => { json: true, body: { name: 'accountC', + webhook_secret: 'foobar', service_provider_sid: spA_sid } }); @@ -206,7 +211,7 @@ test('authentication tests', async(t) => { simple: false, json: true, body: { - sip_realm: 'sip.foo.bar' + name: 'joe knife' } }); //console.log(`result: ${JSON.stringify(result)}`); @@ -324,33 +329,6 @@ test('authentication tests', async(t) => { }); t.ok(result.statusCode === 404, 'using account token A1 we are not able to retrieve application A2s'); - - /* service provider token can not be used to add phone number */ - result = await request.post('/PhoneNumbers', { - auth: {bearer: spA_token}, - resolveWithFullResponse: true, - simple: false, - json: true, - body: { - number: '16173333456', - voip_carrier_sid - } - }); - t.ok(result.statusCode === 403, 'service provider token can not be used to add phone number'); - - /* account token can not be used to add phone number */ - result = await request.post('/PhoneNumbers', { - auth: {bearer: accA1_token}, - resolveWithFullResponse: true, - simple: false, - json: true, - body: { - number: '16173333456', - voip_carrier_sid - } - }); - t.ok(result.statusCode === 403, 'account level token can not be used to add phone number'); - /* account level token can not create token for another account */ result = await request.post('/ApiKeys', { resolveWithFullResponse: true, diff --git a/test/create-test-db.js b/test/create-test-db.js index 411a02d..cba00dc 100644 --- a/test/create-test-db.js +++ b/test/create-test-db.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const exec = require('child_process').exec ; test('creating jambones_test database', (t) => { @@ -10,7 +10,7 @@ test('creating jambones_test database', (t) => { }); test('creating schema', (t) => { - exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => { if (err) return t.end(err); t.pass('schema successfully created'); t.end(); @@ -18,9 +18,17 @@ test('creating schema', (t) => { }); test('creating auth token', (t) => { - exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err, stdout, stderr) => { if (err) return t.end(err); t.pass('auth token successfully created'); t.end(); }); }); + +test('add predefined carriers', (t) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/add-predefined-carriers.sql`, (err, stdout, stderr) => { + if (err) return t.end(err); + t.pass('predefined carriers added'); + t.end(); + }); +}); diff --git a/test/data/subscription.json b/test/data/subscription.json new file mode 100644 index 0000000..e370a55 --- /dev/null +++ b/test/data/subscription.json @@ -0,0 +1,908 @@ + { + "id": "sub_J5d3C56ZGMDZFA", + "object": "subscription", + "application_fee_percent": null, + "billing_cycle_anchor": 1615381795, + "billing_thresholds": null, + "cancel_at": null, + "cancel_at_period_end": false, + "canceled_at": null, + "collection_method": "charge_automatically", + "created": 1615381795, + "current_period_end": 1618060195, + "current_period_start": 1615381795, + "customer": "cus_J5coVe5AQ5UR6h", + "days_until_due": null, + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "discount": null, + "ended_at": null, + "items": { + "object": "list", + "data": [{ + "id": "si_J5d3M9tUQgrnPa", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1615381796, + "metadata": { + "product_sid": "c4403cdb-8e75-4b27-9726-7d8315e3216d" + }, + "plan": { + "id": "price_1ISRinAxTxXxh2fmnZmorCm2", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": null, + "amount_decimal": null, + "billing_scheme": "tiered", + "created": 1615143177, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4au1au9F2Ysa0", + "tiers": [{ + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 1000, + "unit_amount_decimal": "1000", + "up_to": 99 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 900, + "unit_amount_decimal": "900", + "up_to": 250 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 800, + "unit_amount_decimal": "800", + "up_to": 499 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 700, + "unit_amount_decimal": "700", + "up_to": 999 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 500, + "unit_amount_decimal": "500", + "up_to": null + }], + "tiers_mode": "volume", + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRinAxTxXxh2fmnZmorCm2", + "object": "price", + "active": true, + "billing_scheme": "tiered", + "created": 1615143177, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4au1au9F2Ysa0", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": "volume", + "transform_quantity": null, + "type": "recurring", + "unit_amount": null, + "unit_amount_decimal": null + }, + "quantity": 100, + "subscription": "sub_J5d3C56ZGMDZFA", + "tax_rates": [] + }, { + "id": "si_J5d3F3SkebrH4S", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1615381796, + "metadata": { + "product_sid": "2c815913-5c26-4004-b748-183b459329df" + }, + "plan": { + "id": "price_1ISRhKAxTxXxh2fmhsYLgyLM", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 100, + "amount_decimal": "100", + "billing_scheme": "per_unit", + "created": 1615143086, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4at3poNkI5OSA", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRhKAxTxXxh2fmhsYLgyLM", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1615143086, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4at3poNkI5OSA", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 100, + "unit_amount_decimal": "100" + }, + "quantity": 50, + "subscription": "sub_J5d3C56ZGMDZFA", + "tax_rates": [] + }, { + "id": "si_J5d3bcURInlK5Y", + "object": "subscription_item", + "billing_thresholds": null, + "created": 1615381796, + "metadata": { + "product_sid": "35a9fb10-233d-4eb9-aada-78de5814d680" + }, + "plan": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": null, + "amount_decimal": null, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "tiers": [{ + "flat_amount": 0, + "flat_amount_decimal": "0", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 10 + }, { + "flat_amount": 500, + "flat_amount_decimal": "500", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 30 + }, { + "flat_amount": 1000, + "flat_amount_decimal": "1000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 60 + }, { + "flat_amount": 2000, + "flat_amount_decimal": "2000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 120 + }, { + "flat_amount": 4000, + "flat_amount_decimal": "4000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 300 + }, { + "flat_amount": 5000, + "flat_amount_decimal": "5000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": null + }], + "tiers_mode": "volume", + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "price", + "active": true, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": "volume", + "transform_quantity": null, + "type": "recurring", + "unit_amount": null, + "unit_amount_decimal": null + }, + "quantity": 60, + "subscription": "sub_J5d3C56ZGMDZFA", + "tax_rates": [] + }], + "has_more": false, + "total_count": 3, + "url": "/v1/subscription_items?subscription=sub_J5d3C56ZGMDZFA" + }, + "latest_invoice": { + "id": "in_1ITRnUAxTxXxh2fmmch7bUlr", + "object": "invoice", + "account_country": "US", + "account_name": "drachtio.org", + "account_tax_ids": null, + "amount_due": 96000, + "amount_paid": 96000, + "amount_remaining": 0, + "application_fee_amount": null, + "attempt_count": 1, + "attempted": true, + "auto_advance": false, + "billing_reason": "subscription_create", + "charge": "ch_1ITRnUAxTxXxh2fmvtLpMbNr", + "collection_method": "charge_automatically", + "created": 1615381796, + "currency": "usd", + "custom_fields": null, + "customer": "cus_J5coVe5AQ5UR6h", + "customer_address": null, + "customer_email": "daveh@drachtio.org", + "customer_name": "Dave Horton", + "customer_phone": null, + "customer_shipping": null, + "customer_tax_exempt": "none", + "customer_tax_ids": [], + "default_payment_method": null, + "default_source": null, + "default_tax_rates": [], + "description": null, + "discount": null, + "discounts": [], + "due_date": null, + "ending_balance": 0, + "footer": null, + "hosted_invoice_url": "https://invoice.stripe.com/i/acct_1GjPVFAxTxXxh2fm/invst_J5d3z2L367YU08uV3ZStblw4QzWzN4b", + "invoice_pdf": "https://pay.stripe.com/invoice/acct_1GjPVFAxTxXxh2fm/invst_J5d3z2L367YU08uV3ZStblw4QzWzN4b/pdf", + "last_finalization_error": null, + "lines": { + "object": "list", + "data": [{ + "id": "il_1ITRnUAxTxXxh2fmApdPmIJQ", + "object": "line_item", + "amount": 90000, + "currency": "usd", + "description": "100 session × concurrent call session (Tier 2 at $9.00 / month)", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1618060195, + "start": 1615381795 + }, + "plan": { + "id": "price_1ISRinAxTxXxh2fmnZmorCm2", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": null, + "amount_decimal": null, + "billing_scheme": "tiered", + "created": 1615143177, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4au1au9F2Ysa0", + "tiers": [{ + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 1000, + "unit_amount_decimal": "1000", + "up_to": 99 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 900, + "unit_amount_decimal": "900", + "up_to": 250 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 800, + "unit_amount_decimal": "800", + "up_to": 499 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 700, + "unit_amount_decimal": "700", + "up_to": 999 + }, { + "flat_amount": null, + "flat_amount_decimal": null, + "unit_amount": 500, + "unit_amount_decimal": "500", + "up_to": null + }], + "tiers_mode": "volume", + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRinAxTxXxh2fmnZmorCm2", + "object": "price", + "active": true, + "billing_scheme": "tiered", + "created": 1615143177, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4au1au9F2Ysa0", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": "volume", + "transform_quantity": null, + "type": "recurring", + "unit_amount": null, + "unit_amount_decimal": null + }, + "proration": false, + "quantity": 100, + "subscription": "sub_J5d3C56ZGMDZFA", + "subscription_item": "si_J5d3M9tUQgrnPa", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + }, { + "id": "il_1ITRnUAxTxXxh2fmGn4D2s9W", + "object": "line_item", + "amount": 5000, + "currency": "usd", + "description": "50 sip device × registered device (at $1.00 / month)", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1618060195, + "start": 1615381795 + }, + "plan": { + "id": "price_1ISRhKAxTxXxh2fmhsYLgyLM", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": 100, + "amount_decimal": "100", + "billing_scheme": "per_unit", + "created": 1615143086, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4at3poNkI5OSA", + "tiers": null, + "tiers_mode": null, + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRhKAxTxXxh2fmhsYLgyLM", + "object": "price", + "active": true, + "billing_scheme": "per_unit", + "created": 1615143086, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4at3poNkI5OSA", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": null, + "transform_quantity": null, + "type": "recurring", + "unit_amount": 100, + "unit_amount_decimal": "100" + }, + "proration": false, + "quantity": 50, + "subscription": "sub_J5d3C56ZGMDZFA", + "subscription_item": "si_J5d3F3SkebrH4S", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + }, { + "id": "il_1ITRnUAxTxXxh2fmmUTvs8b0", + "object": "line_item", + "amount": 0, + "currency": "usd", + "description": "60 per min × api rate limit (Tier 3 at $0.00 / month)", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1618060195, + "start": 1615381795 + }, + "plan": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": null, + "amount_decimal": null, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "tiers": [{ + "flat_amount": 0, + "flat_amount_decimal": "0", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 10 + }, { + "flat_amount": 500, + "flat_amount_decimal": "500", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 30 + }, { + "flat_amount": 1000, + "flat_amount_decimal": "1000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 60 + }, { + "flat_amount": 2000, + "flat_amount_decimal": "2000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 120 + }, { + "flat_amount": 4000, + "flat_amount_decimal": "4000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 300 + }, { + "flat_amount": 5000, + "flat_amount_decimal": "5000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": null + }], + "tiers_mode": "volume", + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "price", + "active": true, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": "volume", + "transform_quantity": null, + "type": "recurring", + "unit_amount": null, + "unit_amount_decimal": null + }, + "proration": false, + "quantity": 60, + "subscription": "sub_J5d3C56ZGMDZFA", + "subscription_item": "si_J5d3bcURInlK5Y", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + }, { + "id": "il_1ITRnVAxTxXxh2fm6VDudRAm", + "object": "line_item", + "amount": 1000, + "currency": "usd", + "description": "api rate limit (Tier 3 at $10.00 / month)", + "discount_amounts": [], + "discountable": true, + "discounts": [], + "livemode": false, + "metadata": {}, + "period": { + "end": 1618060195, + "start": 1615381795 + }, + "plan": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "plan", + "active": true, + "aggregate_usage": null, + "amount": null, + "amount_decimal": null, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "interval": "month", + "interval_count": 1, + "livemode": false, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "tiers": [{ + "flat_amount": 0, + "flat_amount_decimal": "0", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 10 + }, { + "flat_amount": 500, + "flat_amount_decimal": "500", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 30 + }, { + "flat_amount": 1000, + "flat_amount_decimal": "1000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 60 + }, { + "flat_amount": 2000, + "flat_amount_decimal": "2000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 120 + }, { + "flat_amount": 4000, + "flat_amount_decimal": "4000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": 300 + }, { + "flat_amount": 5000, + "flat_amount_decimal": "5000", + "unit_amount": null, + "unit_amount_decimal": null, + "up_to": null + }], + "tiers_mode": "volume", + "transform_usage": null, + "trial_period_days": null, + "usage_type": "licensed" + }, + "price": { + "id": "price_1ISRgHAxTxXxh2fmUg80e3mw", + "object": "price", + "active": true, + "billing_scheme": "tiered", + "created": 1615143021, + "currency": "usd", + "livemode": false, + "lookup_key": null, + "metadata": {}, + "nickname": null, + "product": "prod_J4asDaODFXDjui", + "recurring": { + "aggregate_usage": null, + "interval": "month", + "interval_count": 1, + "trial_period_days": null, + "usage_type": "licensed" + }, + "tiers_mode": "volume", + "transform_quantity": null, + "type": "recurring", + "unit_amount": null, + "unit_amount_decimal": null + }, + "proration": false, + "quantity": 0, + "subscription": "sub_J5d3C56ZGMDZFA", + "subscription_item": "si_J5d3bcURInlK5Y", + "tax_amounts": [], + "tax_rates": [], + "type": "subscription" + }], + "has_more": false, + "total_count": 4, + "url": "/v1/invoices/in_1ITRnUAxTxXxh2fmmch7bUlr/lines" + }, + "livemode": false, + "metadata": {}, + "next_payment_attempt": null, + "number": "0A22219A-0001", + "on_behalf_of": null, + "paid": true, + "payment_intent": { + "id": "pi_1ITRnUAxTxXxh2fmmyjspFCb", + "object": "payment_intent", + "amount": 96000, + "amount_capturable": 0, + "amount_received": 96000, + "application": null, + "application_fee_amount": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [{ + "id": "ch_1ITRnUAxTxXxh2fmvtLpMbNr", + "object": "charge", + "amount": 96000, + "amount_captured": 96000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_1ITRnVAxTxXxh2fmTGIuTAaB", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "DRACHTIO COMM SVCS LLC", + "captured": true, + "created": 1615381796, + "currency": "usd", + "customer": "cus_J5coVe5AQ5UR6h", + "description": "Subscription creation", + "destination": null, + "dispute": null, + "disputed": false, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": "in_1ITRnUAxTxXxh2fmmch7bUlr", + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 16, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_1ITRnUAxTxXxh2fmmyjspFCb", + "payment_method": "card_1ITRZIAxTxXxh2fmgZWoCsPx", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": null + }, + "country": "US", + "exp_month": 9, + "exp_year": 2024, + "fingerprint": "KDQxr00TBv7zH8Sg", + "funding": "credit", + "installments": null, + "last4": "4242", + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_1GjPVFAxTxXxh2fm/ch_1ITRnUAxTxXxh2fmvtLpMbNr/rcpt_J5d3fiWQsaYt9Pmkw0SEV34LbxYJyGl", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_1ITRnUAxTxXxh2fmvtLpMbNr/refunds" + }, + "review": null, + "shipping": null, + "source": { + "id": "card_1ITRZIAxTxXxh2fmgZWoCsPx", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "US", + "customer": "cus_J5coVe5AQ5UR6h", + "cvc_check": null, + "dynamic_last4": null, + "exp_month": 9, + "exp_year": 2024, + "fingerprint": "KDQxr00TBv7zH8Sg", + "funding": "credit", + "last4": "4242", + "metadata": {}, + "name": null, + "tokenization_method": null + }, + "source_transfer": null, + "statement_descriptor": "Drachtio Comm Svcs LLC", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + }], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_1ITRnUAxTxXxh2fmmyjspFCb" + }, + "client_secret": "pi_1ITRnUAxTxXxh2fmmyjspFCb_secret_6FB5iXrTQ3w1f7ckUWeycmYoc", + "confirmation_method": "automatic", + "created": 1615381796, + "currency": "usd", + "customer": "cus_J5coVe5AQ5UR6h", + "description": "Subscription creation", + "invoice": "in_1ITRnUAxTxXxh2fmmch7bUlr", + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": { + "card": { + "installments": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": ["card"], + "receipt_email": null, + "review": null, + "setup_future_usage": "off_session", + "shipping": null, + "source": "card_1ITRZIAxTxXxh2fmgZWoCsPx", + "statement_descriptor": "Drachtio Comm Svcs LLC", + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + }, + "payment_settings": { + "payment_method_options": null, + "payment_method_types": null + }, + "period_end": 1615381795, + "period_start": 1615381795, + "post_payment_credit_notes_amount": 0, + "pre_payment_credit_notes_amount": 0, + "receipt_number": null, + "starting_balance": 0, + "statement_descriptor": null, + "status": "paid", + "status_transitions": { + "finalized_at": 1615381795, + "marked_uncollectible_at": null, + "paid_at": 1615381795, + "voided_at": null + }, + "subscription": "sub_J5d3C56ZGMDZFA", + "subtotal": 96000, + "tax": null, + "tax_percent": null, + "total": 96000, + "total_discount_amounts": [], + "total_tax_amounts": [], + "transfer_data": null, + "webhooks_delivered_at": null + }, + "livemode": false, + "metadata": {}, + "next_pending_invoice_item_invoice": null, + "pause_collection": null, + "pending_invoice_item_interval": null, + "pending_setup_intent": null, + "pending_update": null, + "plan": null, + "quantity": null, + "schedule": null, + "start_date": 1615381795, + "status": "active", + "tax_percent": null, + "transfer_data": null, + "trial_end": null, + "trial_start": null + } \ No newline at end of file diff --git a/test/data/test.json b/test/data/test.json new file mode 100644 index 0000000..5bf39da --- /dev/null +++ b/test/data/test.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "foobar", + "private_key_id": "17a374747574b98284367f1fd2197401c8", + "private_key": "-----BEGIN PRIVATE KEY-----\nkdkdkdkdkdkdkdkdkdkdkdkdkddjdjdjdjjEVtS\n8cZeK4w128sbogusVmVARsj5/\nw6Ou08xBX+e1rsKkiaGnzuKhuZczwWMuAwfnBtNQhw2ZUJCDkQfk7tT1100CAi4h\nLAWX/sufHNi9h2z7yCAeRDGqmIM89lRguycKexy8MM4NZRxAIrLs7LIGhTczMFUC\n6TxaFWlXAgMBAAECggEAMHFgrcUn0ATiQcxkeplWom9Ki7gTuhO1wQeOqRtTIZ+d\nCQOigARklx0YTQlsqoG/acLSfeAcfyLWEklULaqJbeghl9KPE4FwPX13VPlZHCMj\nyXUycrjlxw4tHpcy9egDjj5c4XSiUQ/3nNrn0q0EMxFleM3Mhhj1408HifBLH3KY\nKCA+fq+tqmS/daCUgs7jEZCo4z8qFmz0npHUcQ5P7SrsYf00Di9RH8m1Tc9aXU6H\nkMSCfM8/UpPvt+lOQktWjDKP2APowuPyfl4RgX+9jNUm8h2y9jYKeyTelQC0CAUM\nW4ZB6tiDbXOVeP9miXgVwv3WUh4Mrwufar5e0DeqvQKBgQDKZ6Vk7CEVfGQgBoUU\neWo203Tmc2W5qnXRh0pa1VwUtgte+D7l8Lwfc0gCfjbkk7EXKQrQ19jtTwZAzvHz\nsgziQ8H1IDLQBBtknWxuOJbNuronZE6NMWNp+ULu2Y4XN6MnD75sPgtxFMPUUuWI\n5CdXPKweyC+BKC99ukIposGWcwKBgQDGVCYik8nh3fX+BkC1xrFGZ4z3jIqZs8bk\nLFDCiDGwAJTXee2/L5wUJIv4UGoO822OOg+8ftMCtDmdrIkOjsXUPrhbnrGyqZgM\nEC4T3CNDiQXHqxw4tjg4eM5Vcwi6KkuhSAcClz/aaZ1T5jk0QGdF7UpqKcbsURx8\n5hGcscVEjQKBgBXPcV0crLv5+XgR+8knBDEAPDqQ+Mc2/Rck8vgywYdhznvfWDfC\n5yKkc4ABRbz/xTdvrsCuYavAtjXJlvzhlM3U61OUsqUDrEf9Rq/h3S4yDtkrz+Mb\nDVFgELxYKR2LW0NcSPK1BNqcmDWK8Tz9CNg3q3xtqeDLCcMMjRCbfyzNAoGAWlLC\nl2bFP6eNu6XvXJnj7JOGYMtR6BQ3FX2VPjM2pdht8QBnpXWyWH4YfPtqgeqdT3Pj\n7M25nfakcsm8FbQyJqp13cwVU6/nPj80LPlJ2h0SU8/6510dl6J1Hfdo1xgiH46l\nGqn1e6wz6ZzlGoXmQrOB+32RSdja54sEJF/V3pUCgYAoGFZAGVBuWqipbBLdQqNl\n1ppdEliEBhDPq4cZAnNx1lvnmFn8D5bqi+rB8bkqvGcR921AMLDadasHX4BJAJw+\nQoSx1wqy9Zsiaz9EzWUxHtnKFOzMVeVz/RJqH8hNu4xb6Lv50BgTztoO+bGIOAJ/\nVaY6N4gOkiAihhQzsSnLvQ==\n-----END PRIVATE KEY-----\n", + "client_email": "cloud-speech-testing@foobar.iam.gserviceaccount.com", + "client_id": "109945388890626427918", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cloud-speech-testing%40foobarg.iam.gserviceaccount.com" +} diff --git a/test/docker_start.js b/test/docker_start.js index 0fda0bf..48c584b 100644 --- a/test/docker_start.js +++ b/test/docker_start.js @@ -1,12 +1,11 @@ -const test = require('blue-tape'); -//const test = require('tape').test ; +const test = require('tape'); const exec = require('child_process').exec ; test('starting docker network..', (t) => { + t.plan(1); exec(`docker-compose -f ${__dirname}/docker-compose-testbed.yaml up -d`, (err, stdout, stderr) => { setTimeout(() => { t.pass('docker started'); - t.end(err); }, 15000); }); }); diff --git a/test/docker_stop.js b/test/docker_stop.js index 5436e7d..11db6ec 100644 --- a/test/docker_stop.js +++ b/test/docker_stop.js @@ -1,4 +1,4 @@ -const test = require('blue-tape'); +const test = require('tape'); const exec = require('child_process').exec ; test('stopping docker network..', (t) => { diff --git a/test/index.js b/test/index.js index c2078d9..ce98b77 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,7 @@ require('./docker_start'); require('./create-test-db'); require('./sip-gateways'); +require('./smpp-gateways'); require('./service-providers'); require('./voip-carriers'); require('./accounts'); @@ -9,4 +10,7 @@ require('./applications'); require('./auth'); require('./sbcs'); require('./ms-teams'); +require('./speech-credentials'); +require('./recent-calls'); +require('./webapp_tests'); require('./docker_stop'); diff --git a/test/ms-teams.js b/test/ms-teams.js index 39f0c2d..3be6a46 100644 --- a/test/ms-teams.js +++ b/test/ms-teams.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ diff --git a/test/oauth/gh-get-user.js b/test/oauth/gh-get-user.js new file mode 100644 index 0000000..fe7ad5b --- /dev/null +++ b/test/oauth/gh-get-user.js @@ -0,0 +1,19 @@ +const bent = require('bent'); +const getJSON = bent('GET', 200); +const request = require('request'); +require('request-debug')(request); + +const test = async() => { + request.get('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${process.env.GH_CODE}`, + Accept: 'application/json', + 'User-Agent': 'jambonz.us' + } + }, (err, response, body) => { + if (err) console.log(error); + else console.log(body); + }) +}; + +test(); \ No newline at end of file diff --git a/test/oauth/simple_test/index.html b/test/oauth/simple_test/index.html new file mode 100644 index 0000000..832d8d4 --- /dev/null +++ b/test/oauth/simple_test/index.html @@ -0,0 +1,14 @@ + + + + + + + Test oauth signup + + + + + Github + + \ No newline at end of file diff --git a/test/phone-numbers.js b/test/phone-numbers.js index b6e4da2..29a5288 100644 --- a/test/phone-numbers.js +++ b/test/phone-numbers.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -19,19 +19,6 @@ test('phone number tests', async(t) => { /* add service provider, phone number, and voip carrier */ const voip_carrier_sid = await createVoipCarrier(request); - /* provision phone number - failure case: voip_carrier_sid is required */ - result = await request.post('/PhoneNumbers', { - resolveWithFullResponse: true, - simple: false, - auth: authAdmin, - json: true, - body: { - number: '15083084809' - } - }); - t.ok(result.statusCode === 400 && result.body.msg === 'voip_carrier_sid is required', - 'voip_carrier_sid is required when provisioning a phone number'); - /* provision phone number - failure case: digits only */ result = await request.post('/PhoneNumbers', { resolveWithFullResponse: true, @@ -43,37 +30,9 @@ test('phone number tests', async(t) => { voip_carrier_sid } }); - t.ok(result.statusCode === 400 && result.body.msg === 'phone number must only include digits', - 'service_provider_sid is required when provisioning a phone number'); - - /* provision phone number - failure case: insufficient digits */ - result = await request.post('/PhoneNumbers', { - resolveWithFullResponse: true, - simple: false, - auth: authAdmin, - json: true, - body: { - number: '1508308', - voip_carrier_sid - } - }); - //console.log(`result: ${JSON.stringify(result)}`); - t.ok(result.statusCode === 400 && result.body.msg === 'invalid phone number: insufficient digits', - 'invalid phone number: insufficient digits'); - - /* provision phone number - failure case: invalid US number */ - result = await request.post('/PhoneNumbers', { - resolveWithFullResponse: true, - simple: false, - auth: authAdmin, - json: true, - body: { - number: '150830848091', - voip_carrier_sid - } - }); - t.ok(result.statusCode === 400 && result.body.msg === 'invalid US phone number', - 'invalid US phone number'); + t.ok(result.statusCode === 201, + 'accepts E.164 format'); + const sid = result.body.sid; /* add a phone number */ result = await request.post('/PhoneNumbers', { @@ -86,17 +45,17 @@ test('phone number tests', async(t) => { } }); t.ok(result.statusCode === 201, 'successfully created phone number'); - const sid = result.body.sid; + const sid2 = result.body.sid; /* query all phone numbers */ result = await request.get('/PhoneNumbers', { auth: authAdmin, json: true, }); - t.ok(result.length === 1 , 'successfully queried all phone numbers'); + t.ok(result.length === 2, 'successfully queried all phone numbers'); /* query one phone numbers */ - result = await request.get(`/PhoneNumbers/${sid}`, { + result = await request.get(`/PhoneNumbers/${sid2}`, { auth: authAdmin, json: true, }); @@ -108,6 +67,10 @@ test('phone number tests', async(t) => { resolveWithFullResponse: true, }); t.ok(result.statusCode === 204, 'successfully deleted phone number'); + result = await request.delete(`/PhoneNumbers/${sid2}`, { + auth: authAdmin, + resolveWithFullResponse: true, + }); await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid); diff --git a/test/recent-calls.js b/test/recent-calls.js new file mode 100644 index 0000000..8dc5ec3 --- /dev/null +++ b/test/recent-calls.js @@ -0,0 +1,79 @@ +const test = require('tape') ; +const fs = require('fs'); +const jwt = require('jsonwebtoken'); +const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; +const authAdmin = {bearer: ADMIN_TOKEN}; +const request = require('request-promise-native').defaults({ + baseUrl: 'http://127.0.0.1:3000/v1' +}); +const consoleLogger = {debug: console.log, info: console.log, error: console.error} +const {writeCdrs} = require('@jambonz/time-series')(consoleLogger, '127.0.0.1'); +const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils'); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +test('recent calls tests', async(t) => { + const app = require('../app'); + const jsonKey = fs.readFileSync(`${__dirname}/data/test.json`, {encoding: 'utf8'}); + let sid; + try { + let result; + const service_provider_sid = await createServiceProvider(request); + const account_sid = await createAccount(request, service_provider_sid); + + const token = jwt.sign({ + account_sid + }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const authUser = {bearer: token}; + + /* write sample cdr data */ + const points = 500; + const data = []; + const start = new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)); + const now = new Date(); + const increment = (now.getTime() - start.getTime()) / points; + for (let i =0 ; i < 500; i++) { + const attempted_at = new Date(start.getTime() + (i * increment)); + const failed = 0 === i % 5; + data.push({ + call_sid: 'b6f48929-8e86-4d62-ae3b-64fb574d91f6', + from: '15083084809', + to: '18882349999', + answered: !failed, + sip_callid: '685cd008-0a66-4974-b37a-bdd6d9a3c4aa@192.168.1.100', + sip_status: 200, + duration: failed ? 0 : 45, + attempted_at: attempted_at.getTime(), + answered_at: attempted_at.getTime() + 3000, + terminated_at: attempted_at.getTime() + 45000, + termination_reason: 'caller hungup', + host: "192.168.1.100", + remote_host: '3.55.24.34', + account_sid: account_sid, + direction: 0 === i % 2 ? 'inbound' : 'outbound', + trunk: 0 === i % 2 ? 'twilio' : 'user' + }); + } + + await writeCdrs(data); + t.pass('seeded cdr data'); + + /* query last 7 days */ + result = await request.get(`/Accounts/${account_sid}/RecentCalls?page=1&count=25`, { + auth: authUser, + json: true, + }); + + await deleteObjectBySid(request, '/Accounts', account_sid); + await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid); + + //t.end(); + } + catch (err) { + console.error(err); + t.end(err); + } +}); + diff --git a/test/sbcs.js b/test/sbcs.js index cd74681..d26bddb 100644 --- a/test/sbcs.js +++ b/test/sbcs.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -17,18 +17,6 @@ test('sbc_addresses tests', async(t) => { let result; const service_provider_sid = await createServiceProvider(request); - /* add a community sbc */ - result = await request.post('/Sbcs', { - resolveWithFullResponse: true, - auth: authAdmin, - json: true, - body: { - ipv4: '192.168.1.1' - } - }); - t.ok(result.statusCode === 201, 'successfully created community sbc '); - const sid1 = result.body.sid; - /* add a service provider sbc */ result = await request.post('/Sbcs', { resolveWithFullResponse: true, @@ -40,24 +28,17 @@ test('sbc_addresses tests', async(t) => { } }); t.ok(result.statusCode === 201, 'successfully created service provider sbc '); - const sid2 = result.body.sid; - - result = await request.get('/Sbcs', { - resolveWithFullResponse: true, - auth: authAdmin, - json: true - }); - t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.1', 'successfully retrieved community sbc'); + const sid = result.body.sid; result = await request.get(`/Sbcs?service_provider_sid=${service_provider_sid}`, { resolveWithFullResponse: true, auth: authAdmin, json: true }); + //console.log(result.body) t.ok(result.body.length === 1 && result.body[0].ipv4 === '192.168.1.4', 'successfully retrieved service provider sbc'); - await deleteObjectBySid(request, '/Sbcs', sid1); - await deleteObjectBySid(request, '/Sbcs', sid2); + await deleteObjectBySid(request, '/Sbcs', sid); await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid); //t.end(); diff --git a/test/serve-integration.js b/test/serve-integration.js index 63447cb..95bae9f 100644 --- a/test/serve-integration.js +++ b/test/serve-integration.js @@ -45,15 +45,23 @@ const createSchema = () => { const seedDb = () => { return new Promise((resolve, reject) => { console.log('seeding database..') - exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-default-service-provider-and-account.sql`, (err) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/seed-integration-test.sql`, (err) => { if (err) return reject(err); - exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/create-admin-token.sql`, (err) => { - if (err) return reject(err); - exec(`node ${__dirname}/../db/reset_admin_password.js`, (err) => { - if (err) return reject(err); - resolve(); - }); - }); + resolve(); + }); + }); +}; + +const resetAdminPassword = () => { + return new Promise((resolve, reject) => { + /* not needed when running jambonz hosting mode */ + if (process.env.STRIPE_API_KEY) return resolve(); + console.log('creating admin user..') + exec(`node ${__dirname}/../db/reset_admin_password.js`, (err, stdout, stderr) => { + console.log(stdout); + console.log(stderr); + if (err) return reject(err); + resolve(); }); }); }; @@ -72,6 +80,7 @@ startDocker() .then(createDb) .then(createSchema) .then(seedDb) + .then(resetAdminPassword) .then(() => { console.log('ready for testing!'); require('..'); diff --git a/test/service-providers.js b/test/service-providers.js index 5cd4654..5471f6a 100644 --- a/test/service-providers.js +++ b/test/service-providers.js @@ -1,9 +1,10 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ baseUrl: 'http://127.0.0.1:3000/v1' }); +const {deleteObjectBySid} = require('./utils'); process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); @@ -94,7 +95,6 @@ test('service provider tests', async(t) => { }); t.ok(result.name === 'johndoe' && result.root_domain === 'example.com', 'successfully retrieved service provider by sid'); - /* update service providers */ result = await request.put(`/ServiceProviders/${sid}`, { auth: authAdmin, @@ -106,6 +106,16 @@ test('service provider tests', async(t) => { }); t.ok(result.statusCode === 204, 'successfully updated service provider'); + /* add a predefined carrier for a service provider */ + result = await request.post(`/ServiceProviders/${sid}/PredefinedCarriers/7d509a18-bbff-4c5d-b21e-b99bf8f8c49a`, { + auth: authAdmin, + json: true, + resolveWithFullResponse: true, + }); + t.ok(result.statusCode === 201, 'successfully added predefined carrier to service provider'); + + await deleteObjectBySid(request, '/VoipCarriers', result.body.sid); + /* delete service providers */ result = await request.delete(`/ServiceProviders/${sid}`, { auth: authAdmin, diff --git a/test/sip-gateways.js b/test/sip-gateways.js index 3784d91..39781f7 100644 --- a/test/sip-gateways.js +++ b/test/sip-gateways.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -25,6 +25,7 @@ test('sip gateway tests', async(t) => { body: { voip_carrier_sid, ipv4: '192.168.1.1', + netmask: 32, inbound: true, outbound: true } @@ -34,6 +35,7 @@ test('sip gateway tests', async(t) => { /* query all sip gateways */ result = await request.get('/SipGateways', { + qs: {voip_carrier_sid}, auth: authAdmin, json: true, }); @@ -55,12 +57,13 @@ test('sip gateway tests', async(t) => { resolveWithFullResponse: true, body: { port: 5061, + netmask:24, outbound: false } }); t.ok(result.statusCode === 204, 'successfully updated voip carrier'); - /* delete sip gatewas */ + /* delete sip gateways */ result = await request.delete(`/SipGateways/${sid}`, { resolveWithFullResponse: true, simple: false, diff --git a/test/smpp-gateways.js b/test/smpp-gateways.js new file mode 100644 index 0000000..2d44fb2 --- /dev/null +++ b/test/smpp-gateways.js @@ -0,0 +1,88 @@ +const test = require('tape') ; +const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; +const authAdmin = {bearer: ADMIN_TOKEN}; +const request = require('request-promise-native').defaults({ + baseUrl: 'http://127.0.0.1:3000/v1' +}); +const {createVoipCarrier, deleteObjectBySid} = require('./utils'); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +test('smpp gateway tests', async(t) => { + const app = require('../app'); + let sid; + try { + let result; + const voip_carrier_sid = await createVoipCarrier(request); + + /* add a smpp gateway */ + result = await request.post('/SmppGateways', { + resolveWithFullResponse: true, + auth: authAdmin, + json: true, + body: { + voip_carrier_sid, + ipv4: '192.168.1.1', + netmask: 32, + inbound: true, + outbound: true, + use_tls: true, + is_primary: true + } + }); + t.ok(result.statusCode === 201, 'successfully created smpp gateway '); + const sid = result.body.sid; + + /* query all smpp gateways */ + console.log('querying with ') + result = await request.get('/SmppGateways', { + qs: {voip_carrier_sid}, + auth: authAdmin, + json: true, + }); + t.ok(result.length === 1 , 'successfully queried all smpp gateways'); + + /* query one smpp gateway */ + result = await request.get(`/SmppGateways/${sid}`, { + auth: authAdmin, + json: true, + }); + //console.log(`result: ${JSON.stringify(result)}`); + t.ok(result.ipv4 === '192.168.1.1' , 'successfully retrieved voip carrier by sid'); + + + /* update smpp gateway */ + result = await request.put(`/SmppGateways/${sid}`, { + auth: authAdmin, + json: true, + resolveWithFullResponse: true, + body: { + port: 5061, + netmask:24, + outbound: false + } + }); + t.ok(result.statusCode === 204, 'successfully updated voip carrier'); + + /* delete smpp gatewas */ + result = await request.delete(`/SmppGateways/${sid}`, { + resolveWithFullResponse: true, + simple: false, + json: true, + auth: authAdmin + }); + //console.log(`result: ${JSON.stringify(result)}`); + t.ok(result.statusCode === 204, 'successfully deleted smpp gateway'); + + await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid); + + //t.end(); + } + catch (err) { + console.error(err); + t.end(err); + } +}); + diff --git a/test/speech-credentials.js b/test/speech-credentials.js new file mode 100644 index 0000000..5c7d918 --- /dev/null +++ b/test/speech-credentials.js @@ -0,0 +1,123 @@ +const test = require('tape') ; +const fs = require('fs'); +const jwt = require('jsonwebtoken'); +const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; +const authAdmin = {bearer: ADMIN_TOKEN}; +const request = require('request-promise-native').defaults({ + baseUrl: 'http://127.0.0.1:3000/v1' +}); +const {createServiceProvider, createAccount, deleteObjectBySid} = require('./utils'); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); +}); + +test('speech credentials tests', async(t) => { + const app = require('../app'); + const jsonKey = fs.readFileSync(`${__dirname}/data/test.json`, {encoding: 'utf8'}); + let sid; + try { + let result; + const service_provider_sid = await createServiceProvider(request); + const account_sid = await createAccount(request, service_provider_sid); + + /* add a speech credential to a service provider */ + result = await request.post(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, { + resolveWithFullResponse: true, + simple: false, + auth: authAdmin, + json: true, + body: { + vendor: 'google', + service_key: jsonKey + } + }); + t.ok(result.statusCode === 201, 'successfully added a speech credential to service provider'); + //console.log(result.body) + const speech_credential_sid = result.body.sid; + + /* query speech credentials for a service provider */ + result = await request.get(`/ServiceProviders/${service_provider_sid}/SpeechCredentials`, { + resolveWithFullResponse: true, + simple: false, + auth: authAdmin, + json: true, + }); + //console.log(result.body) + t.ok(result.statusCode === 200, 'successfully queried speech credential to service provider'); + + await deleteObjectBySid(request, `/ServiceProviders/${service_provider_sid}/SpeechCredentials`, speech_credential_sid); + + const token = jwt.sign({ + account_sid + }, process.env.JWT_SECRET, { expiresIn: '1h' }); + const authUser = {bearer: token}; + + /* add a credential */ + result = await request.post(`/Accounts/${account_sid}/SpeechCredentials`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + vendor: 'google', + service_key: jsonKey + } + }); + t.ok(result.statusCode === 201, 'successfully added speech credential'); + const sid1 = result.body.sid; + + /* return 403 if invalid account is used */ + result = await request.post(`/Accounts/foobarbaz/SpeechCredentials`, { + resolveWithFullResponse: true, + simple: false, + auth: authUser, + json: true, + body: { + vendor: 'google', + service_key: jsonKey + } + }); + t.ok(result.statusCode === 403, 'returns 403 Forbidden if Account does not match jwt'); + + /* query one credential */ + result = await request.get(`/Accounts/${account_sid}/SpeechCredentials/${sid1}`, { + auth: authAdmin, + json: true, + }); + t.ok(result.vendor === 'google' , 'successfully retrieved speech credential by sid'); + + /* query all credentials */ + result = await request.get(`/Accounts/${account_sid}/SpeechCredentials`, { + auth: authAdmin, + json: true, + }); + t.ok(result[0].vendor === 'google' && result.length === 1, 'successfully retrieved all speech credentials'); + + + /* return 404 when deleting unknown credentials */ + result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/foobarbaz`, { + auth: authUser, + resolveWithFullResponse: true, + simple: false + }); + t.ok(result.statusCode === 404, 'return 404 when attempting to delete unknown credential'); + + /* delete the credential */ + result = await request.delete(`/Accounts/${account_sid}/SpeechCredentials/${sid1}`, { + auth: authUser, + resolveWithFullResponse: true, + }); + t.ok(result.statusCode === 204, 'successfully deleted speech credential'); + + + await deleteObjectBySid(request, '/Accounts', account_sid); + await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid); + + //t.end(); + } + catch (err) { + console.error(err); + t.end(err); + } +}); + diff --git a/test/utils.js b/test/utils.js index 72ec34e..4249d35 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,3 +1,5 @@ +const bytesToUuid = require("uuid/lib/bytesToUuid"); +const uuid = require('uuid').v4; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; @@ -42,7 +44,8 @@ async function createAccount(request, service_provider_sid, name = 'daveh') { json: true, body: { name, - service_provider_sid + service_provider_sid, + webhook_secret: 'foobar' } }); return result.sid; @@ -66,6 +69,18 @@ async function createApplication(request, account_sid, name = 'daveh') { return result.sid; } +async function createApiKey(request, account_sid) { + const result = await request.post('/ApiKeys', { + auth: authAdmin, + json: true, + body: { + account_sid, + token: uuid() + } + }); + return result.sid; +} + async function deleteObjectBySid(request, path, sid) { const result = await request.delete(`${path}/${sid}`, { auth: authAdmin, @@ -79,5 +94,6 @@ module.exports = { createPhoneNumber, createAccount, createApplication, + createApiKey, deleteObjectBySid }; diff --git a/test/voip-carriers.js b/test/voip-carriers.js index 8a99168..6892f16 100644 --- a/test/voip-carriers.js +++ b/test/voip-carriers.js @@ -1,4 +1,4 @@ -const test = require('blue-tape').test ; +const test = require('tape') ; const ADMIN_TOKEN = '38700987-c7a4-4685-a5bb-af378f9734de'; const authAdmin = {bearer: ADMIN_TOKEN}; const request = require('request-promise-native').defaults({ @@ -105,7 +105,7 @@ test('voip carrier tests', async(t) => { //console.log(`result: ${JSON.stringify(result)}`); t.ok(result.statusCode === 204, 'successfully deleted voip carrier'); - /* create voipd carrier that is a customer PBX */ + /* create voip carrier that is a customer PBX */ const service_provider_sid = await createServiceProvider(request); const account_sid = await createAccount(request, service_provider_sid); const account_sid2 = await createAccount(request, service_provider_sid, 'another'); @@ -189,6 +189,30 @@ test('voip carrier tests', async(t) => { sid = result.body.sid; await deleteObjectBySid(request, '/VoipCarriers', sid); + /* add a voip carrier for a service provider */ + result = await request.post(`/ServiceProviders/${service_provider_sid}/VoipCarriers`, { + resolveWithFullResponse: true, + auth: authAdmin, + json: true, + body: { + name: 'twilio', + e164_leading_plus: true + } + }); + t.ok(result.statusCode === 201, 'successfully created voip carrier for a service provider'); + sid = result.body.sid; + + /* list voip carriers for a service provider */ + result = await request.get(`/ServiceProviders/${service_provider_sid}/VoipCarriers`, { + resolveWithFullResponse: true, + auth: authAdmin, + json: true, + }); + //console.log(result.body); + t.ok(result.statusCode === 200, 'successfully retrieved voip carrier for a service provider'); + sid = result.body[0].voip_carrier_sid; + + await deleteObjectBySid(request, '/VoipCarriers', sid); await deleteObjectBySid(request, '/Applications', application_sid); await deleteObjectBySid(request, '/Accounts', account_sid); await deleteObjectBySid(request, '/Accounts', account_sid2); diff --git a/test/webapp_tests.js b/test/webapp_tests.js new file mode 100644 index 0000000..2b44436 --- /dev/null +++ b/test/webapp_tests.js @@ -0,0 +1,512 @@ +const test = require('tape') ; +const exec = require('child_process').exec ; +const Account = require('../lib/models/account'); +const request = require('request-promise-native').defaults({ + baseUrl: 'http://127.0.0.1:3000/v1' +}); +const theOneAndOnlyServiceProviderSid = '2708b1b3-2736-40ea-b502-c53d8396247f'; +const {createApiKey} = require('./utils'); + +const sleepFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +test('re-creating schema', (t) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/jambones-sql.sql`, (err, stdout, stderr) => { + if (err) return t.end(err); + t.pass('schema successfully created'); + t.end(); + }); +}); + +test('seeding database for webapp tests', (t) => { + exec(`mysql -h 127.0.0.1 -u root --protocol=tcp --port=3360 -D jambones_test < ${__dirname}/../db/webapp-tests.sql`, (err, stdout, stderr) => { + if (err) return t.end(err); + t.pass('successfully re-seeded database'); + t.end(); + }); +}); + +test('webapp tests', async(t) => { + const app = require('../app'); + let sid; + try { + let result; + + /* create a new user/account using email/password */ + const code = '123456'; + result = await request.post('/register', { + resolveWithFullResponse: true, + json: true, + body: { + service_provider_sid: theOneAndOnlyServiceProviderSid, + provider: 'local', + name: 'Joe User', + email: 'joe@user.com', + password: 'fiddlesticks', + email_activation_code: code + } + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.pristine === true && + !result.body.is_active && result.body.root_domain === 'sip.yakeeda.com', + 'successfully created a user and account and got jwt using email validation'); + + const {user_sid, account_sid, jwt} = result.body; + let authUser = {bearer: jwt}; + + /* invalid code */ + result = await request.put('/ActivationCode/38383', { + resolveWithFullResponse: true, + auth: authUser, + simple: false, + json: true, + body: { + user_sid, + type: 'email' + } + }); + + t.ok(result.statusCode === 400, 'fails to validate email with invalid code'); + + /* invalid user */ + result = await request.put(`/ActivationCode/${code}`, { + resolveWithFullResponse: true, + auth: authUser, + simple: false, + json: true, + body: { + user_sid: 'foobar', + type: 'email' + } + }); + + t.ok(result.statusCode === 400, 'fails to validate email with invalid user'); + + /* successfully validate the password */ + result = await request.put(`/ActivationCode/${code}`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + user_sid, + type: 'email' + } + }); + t.ok(result.statusCode === 204, 'successfully validated email and activated account'); + + /* create a phone validation code */ + result = await request.post('/ActivationCode', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + simple: false, + body: { + user_sid: 'foobar', + type: 'phone', + value: '16173333456', + code: '12389' + } + }); + t.ok(result.statusCode === 400, 'returns 400 bad request creating activation code with invalid user_sid'); + + result = await request.post('/ActivationCode', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + simple: false, + body: { + user_sid, + type: 'foobar', + value: '16173333456', + code: '12389' + } + }); + t.ok(result.statusCode === 400, 'returns 400 bad request creating activation code with invalid type'); + + result = await request.post('/ActivationCode', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + simple: false, + body: { + user_sid, + type: 'email', + value: 'notanemail', + code: '12389' + } + }); + t.ok(result.statusCode === 400, 'returns 400 bad request creating activation code with invalid email'); + + /* create a phone validation code */ + result = await request.post('/ActivationCode', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + user_sid, + type: 'phone', + value: '16173333456', + code: '12389' + } + }); + t.ok(result.statusCode === 204, 'successfully added a phone validation code'); + + /* successfully validate the code */ + result = await request.put('/ActivationCode/12389', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + user_sid, + type: 'phone' + } + }); + t.ok(result.statusCode === 204, 'successfully validated phone number'); + + /* check availability of a phone numbers and email */ + result = await request.get('/Availability?type=phone&value=16173333456', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === false, 'indicates when phone number is not available'); + + result = await request.get('/Availability?type=phone&value=15083084809', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === true, 'indicates when phone number is available'); + + result = await request.get('/Availability?type=email&value=joe@user.com', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === false, 'indicates when email is not available'); + + result = await request.get('/Availability?type=email&value=jim@user.com', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === true, 'indicates when email is available'); + + /* check if a subdomain is available */ + result = await request.get('/Availability?type=subdomain&value=mycompany.sip.jambonz.us', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === true, 'indicates when subdomain is available'); + + /* these hit the DNS provider (dnsmadeeasy) so only do as needed */ + if (process.env.DME_API_KEY) { + /* add a subdomain to the account */ + result = await request.post(`Accounts/${account_sid}/SipRealms/test.yakeeda.com`, { + resolveWithFullResponse: true, + auth: authUser, + }); + t.ok(result.statusCode === 204, 'added subdomain'); + + /* change the subdomain */ + result = await request.post(`Accounts/${account_sid}/SipRealms/myco.yakeeda.com`, { + resolveWithFullResponse: true, + auth: authUser, + }); + t.ok(result.statusCode === 204, 'added subdomain'); + + /* retrieve account and verify sip_realm */ + result = await request.get(`Accounts/${account_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.sip_realm === 'myco.yakeeda.com' && result.body.is_active, + 'sip_realm successfully added to account'); + + /* check if a subdomain is available */ + result = await request.get('/Availability?type=subdomain&value=myco.yakeeda.com', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + t.ok(result.statusCode === 200 && result.body.available === false, 'indicates when subdomain is not available'); + } + + /* retrieve test number and app for a service provider */ + result = await request.get(`/AccountTest/${theOneAndOnlyServiceProviderSid}`, { + resolveWithFullResponse: true, + json: true, + }); + //console.log(JSON.stringify(result.body)); + t.ok(result.statusCode === 200 && + result.body.phonenumbers.length === 1 && result.body.applications.length === 1, 'retrieves test number and application'); + + /* update user name */ + result = await request.put(`/Users/foobar`, { + resolveWithFullResponse: true, + json: true, + simple: false, + auth: authUser, + body: { + name: 'Jane Doe' + } + }); + t.ok(result.statusCode === 403, 'rejects attempt to update different user'); + + /* update user name */ + result = await request.put(`/Users/${user_sid}`, { + resolveWithFullResponse: true, + json: true, + auth: authUser, + body: { + name: 'Jane Doe' + } + }); + t.ok(result.statusCode === 204, 'updates user name'); + + /* update password */ + result = await request.put(`/Users/${user_sid}`, { + resolveWithFullResponse: true, + json: true, + auth: authUser, + body: { + old_password: 'fiddlesticks', + new_password: 'foobarbazzle' + } + }); + t.ok(result.statusCode === 204, 'updates user password'); + + /* update email */ + result = await request.put(`/Users/${user_sid}`, { + resolveWithFullResponse: true, + json: true, + auth: authUser, + body: { + email: 'janedoe@gmail.com', + email_activation_code: '39877' + } + }); + t.ok(result.statusCode === 204, 'updates email address'); + + /* successfully validate the new email */ + result = await request.put('/ActivationCode/39877', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + user_sid, + type: 'email' + } + }); + t.ok(result.statusCode === 204, 'successfully validated the new email address'); + + /* add api keys */ + await createApiKey(request, account_sid); + await createApiKey(request, account_sid); + + /* retrieve my own user info */ + result = await request.get(`/Users/me`, { + resolveWithFullResponse: true, + json: true, + auth: authUser, + }); + //console.log(result.body); + t.ok(result.statusCode === 200, 'successfully retrieved my own user details'); + + /* sign in with new email and password */ + result = await request.post('/signin', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + email: 'janedoe@gmail.com', + password: 'foobarbazzle' + } + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.user_sid === user_sid, 'successfully signed in with changed email and password'); + + /* logout */ + result = await request.post('/logout', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + email: 'janedoe@gmail.com', + password: 'foobarbazzle' + } + }); + //console.log(result.body); + t.ok(result.statusCode === 204, 'successfully logged out'); + await sleepFor(1200); + + /* using old jwt fails */ + result = await request.get(`/Users/me`, { + resolveWithFullResponse: true, + simple: false, + auth: authUser, + }); + //console.log(result.body); + t.ok(result.statusCode === 401, 'fails using jwt after logout'); + + /* sign in again */ + result = await request.post('/signin', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + email: 'janedoe@gmail.com', + password: 'foobarbazzle' + } + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.user_sid === user_sid, 'successfully signed in again'); + authUser = {bearer: result.body.jwt}; + + /* new jwt works */ + result = await request.get(`/Users/me`, { + resolveWithFullResponse: true, + json: true, + auth: authUser, + }); + //console.log(result.body); + t.ok(result.statusCode === 200, 'new jwt works'); + + /* can not delete a voip_carrier that is not associated to my account */ + result = await request.delete('/VoipCarriers/5145b436-2f38-4029-8d4c-fd8c67831c7a', { + resolveWithFullResponse: true, + auth: authUser, + simple: false + }); + t.ok(result.statusCode === 422, 'fails to delete a voip_carrier not associated with users account'); + + /* add a BYOC carrier + Note: no need to supply account_sid, it will be assigned based on the jwt + */ + result = await request.post('/VoipCarriers', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + name: 'BYCO1', + } + }); + t.ok(result.statusCode === 201 && result.body.sid, 'succesfully created BYOC carrier'); + const carrier_sid = result.body.sid; + + /* add a sip gateway to the carrier */ + result = await request.post('/SipGateways', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + voip_carrier_sid: carrier_sid, + ipv4: '192.168.1.1', + inbound: true, + outbound: true + } + }); + t.ok(result.statusCode === 201, 'successfully created sip gateway for BYOC carrier'); + let gateway_sid = result.body.sid; + + /* update sip gateway */ + result = await request.put(`/SipGateways/${gateway_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + port: 5080 + } + }); + t.ok(result.statusCode === 204, 'successfully updated sip gateway for BYOC carrier'); + + /* delete sip gateway */ + result = await request.delete(`/SipGateways/${gateway_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + }); + t.ok(result.statusCode === 204, 'successfully deleted sip gateway for BYOC carrier'); + + /* add a smpp gateway to the carrier */ + result = await request.post('/SmppGateways', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + voip_carrier_sid: carrier_sid, + ipv4: '192.168.1.1', + inbound: true, + outbound: true + } + }); + t.ok(result.statusCode === 201, 'successfully created smpp gateway for BYOC carrier'); + gateway_sid = result.body.sid; + + /* update smpp gateway */ + result = await request.put(`/SmppGateways/${gateway_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + json: true, + body: { + port: 5080 + } + }); + t.ok(result.statusCode === 204, 'successfully updated smpp gateway for BYOC carrier'); + + /* delete smpp gateway */ + result = await request.delete(`/SmppGateways/${gateway_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + }); + t.ok(result.statusCode === 204, 'successfully deleted smpp gateway for BYOC carrier'); + + result = await request.get('/Sbcs', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.length === 1, 'retrieve Sbcs'); + + result = await request.get('/Smpps', { + resolveWithFullResponse: true, + auth: authUser, + json: true, + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.length === 2, 'retrieve Smpps'); + + /* delete account */ + result = await request.delete(`/Accounts/${account_sid}`, { + resolveWithFullResponse: true, + auth: authUser, + }); + t.ok(result.statusCode === 204, 'successfully deleted account'); + + /* create a new user/account using same email/password */ + result = await request.post('/register', { + resolveWithFullResponse: true, + json: true, + body: { + service_provider_sid: theOneAndOnlyServiceProviderSid, + provider: 'local', + name: 'Joe User', + email: 'joe@user.com', + password: 'fiddlesticks', + email_activation_code: code + } + }); + //console.log(result.body); + t.ok(result.statusCode === 200 && result.body.pristine === true && + !result.body.is_active && result.body.root_domain === 'sip.yakeeda.com', + 'successfully created a user and account and got jwt using email validation'); + + } + catch (err) { + console.error(err); + t.end(err); + } +}); +