From f1f51f12e4c7358fbdd558bf2b46c339e7628b4a Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 12:45:45 -0400 Subject: [PATCH 01/25] update(api): enable Google Edge caching. Relates to #104 --- lib/controllers/api/index.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/controllers/api/index.js b/lib/controllers/api/index.js index 04f1978..32bb244 100755 --- a/lib/controllers/api/index.js +++ b/lib/controllers/api/index.js @@ -15,6 +15,7 @@ var express = require('express'), CacherRedis = require('cacher-redis'); module.exports = function (app) { + const EDGE_CACHE_MAX_AGE = 3600; // 1 hr var versions = []; var rateHandler; var cacher; @@ -131,6 +132,19 @@ module.exports = function (app) { rm(req, res, next); }; + + /** + * Enable Google Edge caching for our API + * @param req + * @param res + * @param next + */ + var edgeCache = function (req, res, next) { + res.header('Cache-Control', 'public, max-age=' + EDGE_CACHE_MAX_AGE); + res.header('Pragma', 'Public'); + next(); + }; + require('fs').readdirSync(__dirname + '/').forEach(function (file) { if (file.match(/.+\.js/g) === null) { var version = file; @@ -142,6 +156,7 @@ module.exports = function (app) { impl.use(apiKeyMiddleware); impl.use(rateMiddleware); impl.use(analyticsMiddleware(version)); + impl.use(edgeCache); impl.route = function (method, path, metadata) { var args = Array.prototype.slice.call(arguments); From 2dc4bf855e2db1348dfc89c597d1ff11563359bf Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 12:46:38 -0400 Subject: [PATCH 02/25] update(express): remove blitzio magic endpoint and returning 42. --- lib/config/express.js | 8 -------- lib/controllers/index.js | 4 ---- lib/routes.js | 2 -- 3 files changed, 14 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index b794bb2..999899c 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -54,14 +54,6 @@ module.exports = function (app) { }); app.configure('production', function () { - app.use(function (req, res, next) { - if (Object.keys(req.headers).length === 0 || req.url === '/mu-7420b817-cdecfc50-f5d31e37-c96b5e8f') { - return res.send('200', '42'); - } else { - next(); - } - }); - app.use(express.favicon(path.join(config.root, 'public', 'favicon.ico'))); app.use(express.compress()); app.use(express.static(path.join(config.root, 'public'), {maxAge: 604800000})); diff --git a/lib/controllers/index.js b/lib/controllers/index.js index 4b038ae..826ee8b 100755 --- a/lib/controllers/index.js +++ b/lib/controllers/index.js @@ -24,7 +24,3 @@ exports.partials = function(req, res) { exports.index = function(req, res) { res.render('index'); }; - -exports.blitzio = function(req, res) { - res.send(200, '42'); -}; diff --git a/lib/routes.js b/lib/routes.js index 26cf9f9..dc4117e 100755 --- a/lib/routes.js +++ b/lib/routes.js @@ -18,8 +18,6 @@ module.exports = function(app) { app.get('/partials/*', index.partials); app.get('/directives/*', index.partials); - app.get('/mu-7420b817-cdecfc50-f5d31e37-c96b5e8f', index.blitzio); - app.get('/*', function(req, res, next) { if(req.url.indexOf('api/') !== -1) { next(); From 651b9f2a88a4a1893012153cd868edd4c1a43aaa Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 16:58:05 -0400 Subject: [PATCH 03/25] update(about): remove out of date references to Yeoman, Redis, Openshift Update to over 500 GDG communities world-wide --- app/about/about.html | 8 +------- app/images/powered_by_os.png | Bin 18642 -> 0 bytes app/images/powered_by_redis.png | Bin 13160 -> 0 bytes app/images/powered_by_yeoman.png | Bin 19357 -> 0 bytes app/images/yeoman.png | Bin 13501 -> 0 bytes 5 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 app/images/powered_by_os.png delete mode 100644 app/images/powered_by_redis.png delete mode 100644 app/images/powered_by_yeoman.png delete mode 100755 app/images/yeoman.png diff --git a/app/about/about.html b/app/about/about.html index 473a357..d2d3e71 100644 --- a/app/about/about.html +++ b/app/about/about.html @@ -20,7 +20,7 @@

Data

- With over 400 communities world-wide there is a lot of data to be collected and stored each day. This is what + With over 500 communities world-wide there is a lot of data to be collected and stored each day. This is what the hub does. Running on Google's Cloud Platform we are able to keep track with everything that's happening in the global GDG community.
@@ -56,18 +56,12 @@

Made possible by

-
- -
-
- -

and you...yes you!

diff --git a/app/images/powered_by_os.png b/app/images/powered_by_os.png deleted file mode 100644 index a5f418f41e85e93a457a769f5e2380034ea2c031..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18642 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 Dol+0! diff --git a/app/images/powered_by_redis.png b/app/images/powered_by_redis.png deleted file mode 100644 index 990855b8d239dd055f724cc71399ab8785b21205..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13160 zcmch7RZv_(x9$XY4-UbC+u%-s5Q4kA4K9PbLvVt-CBYdqxD7h^Ai>>22<`;E{O4Ak zms@q8?zs=$yQ{l*b+6ugt@VAYSB#p995x0y1^@uSmjC=w0|0nK`q~~wLwT)vVb3J5 z4|Lbh`tAS#CgFd|8$eFp>kJH4I~f@@HCq=C7k671R~mU485&nN7m%H!H2~nVoUdu4 zrFlRse!G4ptr#7Wrs$&a7L7(jIxd7LnUR$i9Zw;ews`5SdN01LEE2_VQ8Z#=Vo3a3 zb#|;+j78L4+HZ;B-=kwkAGZ9zJIr_9{+@VlT9DYUzRYigqe9WJGCuRF^M_znNK@l) zhQkJiwstu`gkmtb0`Slpt!Uk!=-&YDg2cqw=%A?GfHyv~m}mf~axRpc^2-bEU+HY4 zHz5&kpdQIWifAE3fDe9&(xre8pWcMzXEAC5ijV+iW0n>>05uMP8B_4@SwKksUAF%l zfMFW#+c!nY02)H;=#KzfQ9w0ZCsr1q#|3skZ*#M$X7rJ8SY&CfMtgk1P-5}P+AS@edh{gd%)z#&sW}cKM zVIvYSM=;O%z}e@UO~w<-hr4+>4geG-zkQYV^2uisr)F|eAh8+8l;dv?$_t%^#m@5{ zyvp?h0I=l|H2uuZ)kq#9iW1`V{O$b-lC245k>_n3r~&sw6QJl|S@+WQKW8Ib{IzXq zX>WITQDH#Z(0o)k@Y$x%6sr5=_%BH8@%DPNdz&GY$2e3D>29-c{7R{W5}tw{VX^u< zN$#-)^W~9hmTo}aqD_yLXh)p@ocJ*({*tGJE=D?qW$^vXi`Di9`vaoH20x(AUgC{^ zGDo?wCE|@D6P}(2bo&+nxae@{{l$!i7-Ac~1^0eAm3)#bdJhNz$)|t;0F#dloLZxe zk}xy?;A2rJYn>FuRWB(!6os-Ed9@ew(M%vhnqi<%8c!O-DumL_jHNnEnmKBsj*8Wc z>(>V=KB&5FRH`cuXPU4_9{GWjuv060RpQ$dy-4KMo7=3C_799rgggr)e$^p|t8`Q;rVKvFL zl?#$beENntW#s|PNg^)FopSq0E*>vXygyZCi@Tg8Ap_%v{vk!>9dP949O{>@7h*?G zwH&LbtH!DtBCpe_M(l{WwuIMvBN2qkLeT|~yrpT(sii*aDe0*!elDk>WZ=VJMi)d! zj8N}mqDw7T{K?pk|8IcQf(Q6T?=w67JV6p67oLB#%zz*RL5vI!Q`=Vr`T8GPGXyiA zW}G$X57;Ym`971=r;aRJIW(XOr?Jzp4M*nl=@p$aECCRp8Za)VNFoNuus#f$#D>RM7XC%Q7#6 zV%LdIS+-dyO8J!9fYwG1l^K~CdR3pR)T`tUy98>E%koRRWIOa9LUE*E3QR^!ayChm z$oc!4OPXtC7-huD;v&~IoRtnG6vZ7My?dO?PO=KRO+pHkT*}scyY}xX1G%2Go}BK> zuH~@wP#UoqP*$)eNyP^F!7gG{T(FgMbs6LS4>Bd@>E@YR5y@@SV&CUJC49n-w;M9r zVc2mRavJ*1h{8zAsHpr``6Nd}xvi9?R8&b+2{ti4@oi#0n?F|^xWKcYyPvy|yU}81 z;A6nvT;AMjE#I(fP^y2@-02)?Ago`ar)^-|_z98^c|YF)=_tc5Yg0*8sVbPa>90F) z9%-&9Oo-!DbVM2pXwi$*;L<~mc%Sf=XThr z4pNm-4Yg_7r{}WdO4fxe)U}j&SKgYRl1SW0xE1b5?1%}j^Q`NejtfFthFiu|`&|l5 zBR6D%>0hi~{9dL3Wg+k1*dWG)N`)FCJly#O(Do!simX`QG%OuU#TT_ny_HG|gM{t% zLTGJ=kG?`^Z}+n%lPB$PBGrQa%uk`3|2B{755i)K{1T}fC7I|Ntw%l$%;fi;UX^j* z_;~*@B#Ud<*wwdqvhOOgjJSf6fL$cxgHp7LP}-Rk9&0&q{QC<+@AruqzI{}E zHgsIm0@KlpGDfV_Di@zwmHkqVk~{f*83uVv*!S8Zm@2+@Dq$HDTSqoL4?4%Sa8akDjyN=atwz@8z-X2({i9{Tyugau zU}LQd>Y0cc70#$CV4FA|Zya6uDwua#_;O6pN3fOBToR?I%R#00yQ|t>xIql;yMnRv z7MU$d2c=r9HMW`6X->83SU0mKsEg50*&)=VcQmZG>5ao7rZ&c2WZ>cI!m+Wc9+&<` zllgsAK83{FnAjSk6_U)i!HdkRK6?~jTt<7cSJQr0R1LIQ!9g>igWu~M1%@~om6^XY zah$Q2mVaVz==?mszL8zw%A8q_fhOW~e0XOxx8$DE z%!|y_9;bkkr@zocvf7oQz9H)F5g%Tl$(7E-aj*BPH`VF)4P4dT0GeRt7u$dJ*P2sh z52<9SsV_)~S(lHNyc{d9c$3ERvxYJN;P)N?2nh!O{=K|D4*&p9ZUEq~82})X1ptt^ zq?itV0sxp(N^}bmD>2-kb$Di@uk$S6u7}s@h1GNwnlW| zIC5ahp1pI8FP~rJA))fGEL(!<2%opD_6RwfoVMJYG#Jci2#EdTS63dYr)%1t^S96E z-E*32n%c%|jZ;EB@XKGkaIO4Gk-)!sr~i&?XAJ1k!d%5%ck$4|Xb@i8Xz?`vyQ1R` zqlt#T_WoD(KR<-_-wXa{t^eN3e|rCa4gHt!|Gka>Q*I^#m-j~lfi_O26)+p7Ga@q* zH6+H6LPSZVA!-N7o19TB!9q8Yg@PxddwcD)c{k)ojLV9uXLTeHaszcMk`Mmb;fIP5 zfpDl`2>i`PiVWkcU~%Wto7zxCNYhMW0`%}`#|GMTb6!XJNeVBt;*tU?3uOz{1RGx* z*bTeq6;Kaf&YxCykSOG1=V5ogYsI-EKRbo~$TJB;8y|s@Z_YsvLzyOL;}z1+u4d&? zZ#5s5oL8HyJ3~YJ!?uO~Fw;?yN$FZXY6|Q`AbaE>v_el8VM1rYp;e)UqjrpW)p<(# zKjMy%U+~TZF$a-pD5wTE>I(7&gA1>9yK^_woIc)4eDrvc8SuYR>+ly`x?|oe6Hk~Z zd}P=w%U9*`DO&P|AaJ4vA*>>G;-l?)5UL2InY_u4*vXO=A@QMh1-h<(GqfR_4)D#?FHT&xzm;xVH^IQJsA$ciGROluFxGnT%9C?;novRXE%6x*7RAXLW z%ME&@vd>Jr37qj1#g8zWbPeD1pyr{E155`74pm4~WJM$*ve>66LNG;B^^p~d9q{ci zaJx2_^4cC~PX0Dsb^Y%m*J*v0v_5`xe5>Kt$LW#calw%rBH57C5gOb#;E1x<9;kHY zxVeH^&_Q|!AzlfdF8JcPj+mFk0>&db01oRzzd$gr+7br_J*VwHHF$GPi+4=RCp6Ut zzt5F;wsT#vbty!>b&ZW`wzqi&&N^McHQEbb5d2JhfJGu-turBo9igG#YlFWs zYs&5$MWat1kpGQijoxP3JIx{Xx9FQclDiff&9cR6hJ+H=8b`-8@?TUFuSnqh*<%>x zme4`|Eev%C*#MpAq|YOyI;5TMTwSLmBmlt^HXlM~XTw)oFj~rz`j%83|2_BV@2d$N zQdWAe5ABlFSZ}u3t!o^r-$Bjb z+~YVpZno5>8^4s3$XcQTjgJPr$)!pqEa~j~OoWz73}mV&hvxqEvJc~c=<(N(f5c_z?kt!)8{hr=;bbxeXEG|1D?mY}7i6O{;T!@4h zKm6nR7SQX0SKvopsb=p42ce&zpjJGd^<@2sh5q3*)RWH(3#C z9{G~$xOhYrBbHMlB!Fle{`yuO1H&#HD9LpL9`zTlF{>p|eB=F=C znycE3SU-dGdk~si?LS4Z=lg0i@Eli0qi};FPWHR&cDU4PR9g?R`iyPr{>}($JRLx= zo=4uP{hM}*D9evknLz=LqN%J&UBkS2yyjr#t6pp-rsk8!Iw5651v_7DCu~!A3K*3b z(3`|yO`IvGtVV}J=|;@$u}Tg%S`Mp{hC4SgU=5BSiAaR+_4ynId|bVThE4imDEU*J&XA7TdJU5(yoo+CB$& zHaRSvr@QPpt@>%18j0Oj9Ni6Z9CiO$s91C7T3>!f)-7dR>`q5K4I*)F!weq#R4*SSBq1NX>1Ja3&c^e06QGZ| zHT7Xrs3+YnaL^;?x%)fuSP%D3~w({*w!c5y$JgMn2Ow=!#EA<#qE(avVwl*$y+pQqA}{T}e~a=QfahYiGkqE|N!oK?h(7Pt&#-2}>jazu{CMAEkt-StP1qP?_C(%j{MrKX> z^~;x8BRoT?5ixxHEPupc-jre`@lD_O3DG0kO5xr@Wk%aSYdF0!6s~-z8})~>nxfJI ze<2PdFQn-xm9oe*zP(wnw`warYL7EeiX3I%p%83%`NeyXll}LjQ31Nx+pIn$CPU>@ zLtL>pyyvxJ&M%&uy*RjAI-8W69!i-Vaq{O9@dwv!Uv_j+J(|~UIkJBLA`*&5pL!-D z6+u}TX@orVEP@>e0i}f}N=kJ?b%p0NrO7vM@Uv8mkdbdE#<}&X>WkIPWN3ekU4FjI zD<8?%FRM842-F*tg9p+Y27%iyr{WG)2pRmD2W!D8E2G ziQkfx@H-cPU$Rw;bWXn61Q4y6m_6h881}!s;DSgOZV&Hq9wivZZW(e&io0+$duHP$ zHhGky&e)eTRd5jm5#ON(xnij_F(J70Y6Z6()+KODEx|1fM1s2GPwyER&DHl6*o5+t z{DX0MRd$V*Nm8B>X5lxt&|w9lthdw3Pi@LoWg#a*aQUC5E0?5bGAPo85^y<0^@3!s zRc)9Q40v$H{ZA`?1KM9B{d;P&1nW7o#4z&8NotF~c;!LGd z5i!X_&n+)a4SNMuwd3FD-O+{Kqqey4KSyET<+k7zJPWO^Q-6BdI_S`lypHd^3;RkM1C}Cfn9U#9Ns_jaLbz`BP(e4wUiFiq6;|GCl^F)-W-a&c$g}{XT ziBlSVg&Iodwm&Ude-F#lAU_{if)MYVQ}?|se-oO9u-qU1cnZXpn?E#4w7Wkv9kO$y zI>S-Yy?47KJ-e~)JmuVEWy2F!HVJ-rwvG%JN4*23It3$$wM}@7Ph!j>E3U#=I)dLh z257Wbkl+d7yuq^;#+{sOsQp{ViKS11a&D213aY^)LUNYmnLRaFD zdTchkWu3;384-}0DWr6>UC3+Y{Tjhf#!I6#oqVgrf~D<$+R&F$-*Y{-$sO1H#JB^_ zpb_;v>e_)cuFmpEv}X6nnx^>~HzCx!mG|xTJ+JfKL!FhD{b=D(4~LGiMHuxa7tm9~ zrA`nDoNRysrjiX&tmOBv(#H8^@8M7F&==xq{<-Bar8uVHu@1S2f7D%lZB1xx4N}{} zH+aBG=(voP7{0P%gJkvK_2?veCg|9AY#QFu{w{C6=lV+UIlOo5CCzN1v`r^c2b>dG z@%w&0!C4I_9)>O>z@|OIL-~rUzBJ1CW%m@%ke6I0_9-~6=6G|R+jQ2_CY~8@##_4D zdmwJq*TBj|7N1M9f@&_)>AESRG_dNw1kdiIXT+Rb-Uca8%4OIoWG zKvtj(jwi<^XSEau6bF16^#Q$$PqxJ-RHb?ENE`gBD!JNT3RqGTfyP z5Zp(!*@OW0HVmNLkn zpCf-NhTn7lfL}7Vac0jehxc9f39K3K&c2PR^^r^!G*Uf{Gm(ZXnk4ps-C?2~rwxK% zj|z)?RQC1DJ_&wi8Fb8WDs)m(=8>fasZjBvbw)Y<~jJ$CiU$7Pa459_3IlOE??KZS!nk&%_V1 zpk|(+OyKYPb|F4UI!#?odez{A#{>5Z;Et!Pv9quM1!(YM$LTR8SwJ9vse04f;F#=$ zPS^7DVEx-ND@%cQMY59QjL@e?MtCrBYLGK56Zqy(6j))3o9Y#ea>fL0CiFPzsh@D5 zhl)C}S8@L6nn(8IWh7jPU-@W8UJhDfgTS9vibxlg-G_%X znM27p%65G}X0xp4a`7yCh`X$!QP36^SZud*@qpJzUg;ary~#iFdrn{ElN`@Wm)U+) z(t!O!JVztzZQeLN#R83p=9+!mRT^x8^&bbgVb!q9^;72ceU>r^`#=3C&dj^@=6nYI zPx6NjKoy4=z ztzW}+T%V_7Hw;yv2QPQT@Qt_;Mvm(I?4Rd3r;AUWqeHHLDFM%!XiA7HitpX7Os#To zK?cEgbn={e6FlyrUffYLElXue4zCK1HAkm|OpRzB+7#*%H?5>5F)1Gk~4LTndY z&M+LX`F7T*q7~_9vns$Fb zELdu9HVT{_;3C8((v4ow8xlDFUD@a)Gi7P02S!O=wZxlVlKGk5yXCpX9JmCfp8cdZ zrZ@K9G+tcSIJV*6rHGX7i6waA4i6nJJb*7aGRx{5(>o{WK<~>6r$%=C7UNiARHne&+ z6>Ag=Pa7-LHq{&viY>v?{9|ZN2TvBi!uw(-_chi6yWy{PPXO)DjaUjOz0YeiZDrxh zN)NfXq508lAp&-9;&Ey0WVf85VELxVkG9_*h?e*o!Qlw7;#FUP=;0yVcJU;Lfad(6 ze^CO3P%3Y2&@0-bjNe&^9~^1wo_);n(T}Q>jZlx^yoFAqkGpu+O)%zg;inpo?c3hD zK+nQ11?o;_Gl81VhF~hW8jLCQOe&kuf1DG#jIJSy$2-IwYY>5Mo&n2<^l4Ur3#-WF zYW4D>$z6?xBbv;7BrTzQU$4PH9d;tC15hYV52--IXZ}5zDgn+Q@Rgjkb6e58iYA^H z73aThowHXi9cuQ#^2I61=sVEv_Y^qVZKB&g0kNsycy7!y-0vs|!iAJ(OdpcYm)jzK zbBPEuH-!wr%Ro^(V*T0^#`0VV@?9Z0Zi&rbcR-toIL!`C($Yg)p^9Vr6Lc`8aK)Uw z7S(2i*%}0|Ho_++plleIs7l#O4?96E!kye7s<3P5b0r zjQ7o?$)C3QaS$2K)V^=^$<0z)sS*Rrcks9^WvZTTpP`jfSOW>;hcxR|Nzt)q`}A%a zGp9|%s-cqk@j&}d{WDN zc}L8DoSnfyr~x*rN6UWGql9Y5j~ZTGU;vSaUueM^t5(zqB^9pMuYO++lfquBvWziL zn7Y{~V3+B;Q`d*D{fOHysHMP5lube?Eq)eb{6}?()DlOwj72rH11~E3CG$9G%b7`l z>_YJZS6!R!Ck8B8{4d)P+3dW74vJ_PWd^)jRp2_}=|lh5aK!vugm$q^quiqUd2(x; zBZP4)vLvl4ta-Z96rv6F%Lb`9;{i{_3fSeM#afXBJlEf8$iU`Ow1SF~Xh(kZ*&v~q znQqcOHg2FK5$NsFx;7-YMO1k!(8Mb8h^!_a3-7rVI(=c{Zeq*i*y-9|hP_+4HDyC+ z3`sd8uok{mQgtuVv9v6PlIK2Ekih2*5mJl@_lGJEa0BL%-puL`xpHiq5%K1i+Flt| zml~@rI-y~xma*)B`^+XRl}TrI+K3zlnCEirGkEQctC!g6)nc7pe4W*dhZSlLNNcSw z*NQ%ou+QblfctuxK|-;=MJhU~F})BBwQWRJYwnoyq{Fe#vp`Ced;9Q$ke)54tg9E( z`m0ep?qkXYJ%{RCp1}c&ivaD5X&=^@mQD^o<;_g#~=@Rz%W$0jFjYcA8p3A1Jo@CiFbhHl{tpo-=S)^k!DLd)M|n z@=eVlO}KH;pY2nStfLB2-5(pE;Q2)%dGQG&rb%}ThvZk46o{Zr9YjE-=asZ(l?PYL z*6FD&dMj~jt&pm%xv{q9!`_p$OC6FoN^z_fP4S;hYNcGLm5jO)O$?f2)F`4QWz_MJ z>*h~6=k+CIVMV*g-|U%>v!`vz4ji)|N^mR?2q%}^;_7+ti)Tsq?VBTXc4VB91v~BV zbjX?+82?eg|7D2qEjnENL%on-^{c1* z+{CDc1DR=0u&G-yH#d0daPUpkm^%C?*Dht}AjmwMb3X7BFaF~YGNv}HKL5tsri8q` zLUlN6+{?JLu*?_vxZVPHLUUJEDfXe%I_TtmQr~;-goDHTu%{OlCBTz@)=l&v(mq`8 z&Df7>qEMD*w|sI#gwolxa|UylIcsISI+3%3+rZcYnY@X2jbwYm_>9V3O|NW~l!u(@ zmEgase;P*!rs4SHv6h|&8+-9iUATsy3L+7lHAM%X&Z@4L z3h)IMgt&1nk?eS;RF~Q&w5=k6)Tmo2R3A3_M=5AZ!Wr<4$4jyg;X54+`we-^1c7?| ze0M2QS*3lmwB7a!Xg-)#=XglTc~8Q{;yWNKFkbHD$gc9a=UFDMT)(#D0|J9ud7ic(gr7vnR zbCWYsmd}wU5LWuCY>M1D8GRRm$G9s`doHST)&tq6b~>!uoO7}5J;I~tHQ8H{1j~c+ zrVKQ1I_0EczFoDyl#dhBzq@0kwOx-h@lZru>4H>$*&_tcN}EBi=$;l)cr!;UvZNck zI85FRc~H0HFyGdC^@u=X+QPoqN|?ltWt;weQD%$CM;QxVmo}rsCms8MXH;l+%vQ24 zm|FVc&N$Zhf#p%QK{8>`UA|_?7&v?LK(SiZu%Gf)f+J?n;m8h;v0De7xqurbY76-CwUw7=I3bh-BHYtBcxc+kN9ZNOccYz!v(3QKCO}L*;~o;$Zv){9-fDw?pJ$XY7}kG&?KYko1R+ja*mnu)=uk8956bX(Edt zv_5ZcR=MUnm9k`ByHN>yDJSWG@3{4QJ6L604Ir9mK&-rV=RSLIO1?Sl72kH&jtLw&Vkl;Jd zhpG?gceBjihsVsn(@MPLF|7!mFbvSx_BCp~eG}B_%=;zw{iwX#x1jl!>7cxRNwXo^ zfWD!(IQ%hV;%gYwgP14dT!Tsz9o2(WPw$Q%49@Y#e?7x3bQBlkD!#5+4<0}$bMdp| zrtS3}1jb3Dyx-_r{ytevA~UbYnI#9KgN{D3o#2ON$I1M~-81N+U&u*0bo0~Ao{0RK z%=`+9fZ3&tDl<~&t9`=24jZzdAfyR{b!S@f##G2 zr@GSDXgnyi?w2fg3+GBN^KUbbZz)`B*p;=_YOa(5B+9Psl4#?25O$j(0}C9r8zY>- zk;9>#T~kBr!AEu(7`haJ%xhe%Wqb-hV9kW|xIGu%L^2}>Oz*oUx zZpqk@U-1vyl^Dhx(1(A5dXCFYtR3ui9wdyLKlXtCebsW9@e9#mp)470Ds4AAIeCnN z)vVvDe4>j@?D0PQSdE`S(f1!E~j#iE{8JAMJI3 zJ@X@BPu5PHQ|I2QY%^QqRf$MY^Y9@FB(1}TLSp2RzkGSx=3Tz)7>}h<$x>zAXmwr}0Zc@42m%H#M(}QjAiUpUZ?{rYr#^XGhS=FID_EI`&E> zG5C}=cJ7{s{9F_it3`gI1JzId6qyO0L`r33xRW|Uc(PiJ)jJ?L2+{IEIZo-?zq(aQ z??kfgC*i~iqx4#{C?(Gn>wjG1_KAgMo|(7P+w5CH_sWSoEyvw~QS;w9MfN1vcIVPx zZ>i|gwY{iV^@of#HCKVcgX{L`14EYNh@JIacSLn@MxL z0gXkjnTG^IIQ{Aa#+tK_9`>=~>C3C!Ct-(bj*DL^_$+T7(+ibUz_-8n(%DNo&E6TJ z8+LhD=2)kRC*|)EsQx|PIy!NlN0Ly_09ry?{OCcMhufk!ug1P+clNrUqFcAF9eI;X_A+xX7juTH7gAicbend4vGif?F&PO65|*5KThh- z3k(f9pXYE2``hCt8btl8xjVBbJLW0mfn;UwK=(*pEH?WCy(20y!6xczbOZsS@dgG= z-VX`-6sWTNW27;bQBLk=^HOR)w8>Ae^33eD6B&`{X7iU+=?^*P;4k^eF#(Grou_hW z%*;Y5eq~A2!aOl*V&te|l@cajV?=hdwg>P2-TpKd(Rs6XARiRa8~J!~u6knS^!Ju0 z80VO~l;n3N?2M@gsqy)O*LEXNJRy7cMt^*iP;LwWT07b(eIlqy%_4&q_Wu~@waRkt zwF3Wr{AbF1GeG1J*AJhO|K$jIqvb0q_YRo^vRK2}V0xX$Q7mDWNX-0z z7hfp4Uk>K1Hxal-o;bd}+Qy9l@=BPZ9HC7aHtu0lcJ%?`C~1Zc-_|pCmx^4N_(0+F z5pO8^zRqDgs|6Of4oPhl>~N=AQ^wQbEU&ZBe{&X%tA;=UM3rw#b+0e;H#(7~eB?V9 zPbA?e#!Scg;3J`@E5!N8rO1)Qh%CpEuU&YjB8eM;jAuyWDa=n<|5{LwWc}GDZ9WK8h+t7fB($}9W?)rO~(H_ab6@NZ**tpeFUI&;x;(g=x7OV9FTp)Z9 zQ@KIPeLllsW+l8S8c}_q)tHM#&0apd4~VQ6cVa~uanyOsdE>GsPY{u>2Mjx7qvw`l zB5@03R4kn+I(~g;PKsNQt#k|&k|Ngfol;4z>3bSkAzqYH=|LVQPp-8))turne}Sn} zm7Xcuk{0tgVdoN~9cTlbC9in{&$&)vbh38((~5MG!rN=w1WdiLRi=!Hm}A|mkpCp$ zNDzvz!W!y}2rBoGW0V!|PjMiPp^N$+O}$~$^){#BaGRBX;V4T`4}Yy@tg?nD7ZaI~ zC{amSxlGF3dPhdRK+|;s>P%>Q4v^&-E9VD?^FXo{G)12;>L|24GBr*5Xvb{_6TjH9 zK}N)qRy`+D8p2!&(8nb$GtBRJmAuT)b9su0+(~TTE4OjU`wT#q_9PMJQE1l+^bLMc z6D3~cF@O2M(o7QP$eIPr?N=Xnr6jwR7xtNa9Qu$z7MgAQis+VJ5gjeo!MWH4^7-33 z?QQ{zpFJQHkTL_hdI2G`O@*^sJ}UQ8jDyhmcNXL=ARD(?yURwz0p}C_xLXmGU>-8I z(;Ip(9-~?p|F3-^)C*0Gp2|ys7W)H281qPspax+M*CNo6<2(;9i>hUNxwzc4_1N9Q z7uvOE#SUQiHeajMtlen0upl$CXmmh!pBo#9Z+FgZ6oZai4hDz3y)-!#=|bl%FQ;E+ zBs{y(Hb#-w4F3;vOyjjl*u8gEYaR0x{$4fisADFgTmdTO9BNcdeGR9}=3>-(Yvg|T z8B=U)#;QzKe~ifM?mX3SNY;ZKfW@o#8SkT6BJBe=udqaje&!waPgC+MG-+6|86^G7 zG2q-8ZVjPQYH~#|c``L+K?yT6nNTck!q14}c^3WTcZ zvS1|2RKH#U@JHF>Tgpp2BMw@-==ZrXmC{|Ba=)#9%}=^yZdG-G#T!3TDV+`p=XbZ& zF99dry>wZRs^&=15a!=X>-_W5iJz1|CjXUidhK+q#Ib<&2|r1zGX6cfGpg~LQ|ri3 zem8sBcN&C7o-8Pq_p?xG&yEJ0ka&L3hn)uI>peaHfFw_;*9coy)g7cPR}=Dj3HM5i z=)Jb{rQz=388nWUa3lTG9DI`$j1>%DbE+fsO!RD398WT)s@;6k=A|r`oLCjCg z#aa1}*1lF_UIkxChP51FYZoGK@3B2`A?QCzNNmwESs`br+`NVxvDx?aJfD>9iqq!e8vm?_jc3o!H1yq`?W?!-Bl?%#yii#8Jbd&e~Qp! zw@P$6?jFC#0g3e_mQ{aoQ2X-vuJd^HT$P?w>LAkAG*md|c^+;ye=YuQHs>qV4*W#| zY&;soP26sD8z&co_hH%I3ksJYZS4|giTHCriT6a2 zsr&2@Yy|T&8eZ#cin_6)hoLF4{l$zBpKI5EF#bt0(?JfPWGuK0p%js_oropPWCHc diff --git a/app/images/powered_by_yeoman.png b/app/images/powered_by_yeoman.png deleted file mode 100644 index 4b247694119108387c3ba3d9511028f8c177df24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19357 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 diff --git a/app/images/yeoman.png b/app/images/yeoman.png deleted file mode 100755 index 92497addf96c5c009ac24007f55eb989f3bf74c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13501 zcmV;uG(yXXP)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@+Z=rn|HO+DhmlDBoIatm|!fhjg2uGWbhq^bHc!#?=WDTFl)@& zoXIx9K5)SQ!vZ-`+^PLI8p92OQ1A>zw`vN^+*z2hG=X5&#Fe>&rmC1NA>41U*ojHR< zgOq#|l>0v1#9Yb4KhU2>MZ(pU7gz>5?onTP-Bldj7cMnQbGdKxy(WEeFN*b^@*Q2=Rr-b!~{4F zVKSc5dO*Ow%(@ZlaF!{Hl#aKT-5Tr+q|2-P@lI`aU5b(fP>VlQ*%fH?RyY#TJ7OIe zipa&CK)Xk$QmIwQ9#p48i;iy0ovEYJs%?u#>Cm)A=D^CE4~?w6`8Uy^wJxL?n_{W( zroLCMT>t`scjvk4?Lwzxh3qGpo`MTtV%+Vt$BDCFCa%-I@+jh?0%Q!=Xsu4O23Fqm zEimEsqOy=?#uAb=H3=0-QO7$QFGXkGXy_>?Gfb3e1Hne4f37N1(mWaszMnPlOz}@_PB&u?~vqutC(U`D1(sF{{XPz1Jo&4l@VtSs7kX7QbG$%xPBPg ziFKLC>f389OYTf2xc)-D>4w;St(xBUmbR;{^%wV?|72;^nGe3kx@(f9%2I>mtRt5Y zG2m<_=5y3VKDbgar5hzWV06(ZgZyyJ&sQ%e&0srd&t;A~Igq;l;C;`?eeL3|r@uKS z-PbiRanZwZ){U&Z>6W6hqHjlgV`+iJX#K5P|F-23Pq6BW@Z6I(nTfz`ZnU(7NTSWq z50yk>o2ulP^1Bxwy_CK5dEYyMnTKTs>&lv6UYqLN@#AFkTQ83NmuDB+>+h_LBobSE z9=$AWX_mx#^~jc&V&0nB_XWqzI6N5C7m5xchNwk*BR>m&?CS5&ojLC8cQ&=%GGNFQ z(20|LFH+87c;qIlN@{Gc#KA{D*wLkBZ3}c%i)TRsS`Abi!XQ%52U<32 zDH#K%OtSqr<;+M4Xx#&4Mn+v_RY-@HmMOLm_Odb=hma9CG)ShjX=&eoOJ@Z#yXRp~ z)sWp=4JkX?kr87~ef_%ZjMI%zb9IP--kPSXqZY{1iIfV#I%9V465AWLi)yD&6*Dc- zflS6qB|OZvy?zSLYhu9PBVeQU`wX1+DK8(K{PIVFgZmNfIw0 zixwj#)-8_U=)#bh_|G)GaN(iZL0FnHhc$mF9Sj7*Eb#gPqLR$440Yr{t^g__by1t5h-kF>`YqMJeceL5Q<kN}_*h6Jf+eoV zhBy*g9zYkM^lUjw!jFI*$D+t$;JF8fp*JGT_9#6>J?gNhKJRl!9`0D1C7e%N67R=6 z)^wj;I>GYT@hOrRsLI$laker@1!FXw`Oxl-_R~vV((bSC)B>K2p4i!->AO~nC*9L{ z5@_H?Xb)*BH;%}y9Ny6xtxmVYtEuY51ZNj&6RHtym|BDaEX6WOe)iv z@)^E(+hz%c7QiO*h#n|{IDp(>%<7dZvE5Ti~$RZAVoNM0F%=42|FxtNKYS_m5;h$ z4y4TNKF7O#959WW1eBc~&+ih6#2BEy0u1WGsr63~<{IRxIs^+3BgUNEuG*$t9_x$E z3zdWpvd7Lyr#c$-R9BPXEguKrNlARiYVoGJZQUCHz0B{R?yv4x2c4@ zMf{2BV?#gOx+P*)mucEUyU`ATETB0xRTfZ_$8MlIHk`K1Lso;qLLze^V{g-Zh7mH) z>X!y|sGO{Uu+lEc!J+dBQp%9&0bN@Yj$2kUw6Bp)8-N9-X+}``dW6>^{3ya)Pq~X8 z6<}9K6x_!F(RH_t!-?jfL#3f(#k6Ihr`NVqG0jRRMf3Q?C@Qn{o*g(Lc)!1GXa&F;YLk@++MS{BLaLJ;4r8a2#<@dyyYuc zY%cQ!i{@ouBp5nQcCth_Llb6rI%Bb`Mc#fn^5)I|8SRhWGI`2GY!lWEgn9bzY> z)E!1WOs;JY4&6xHR;m zXn%}@awQf(^lFLiuYn1EE!o)xYdk1c$z-v^<6u|W66@=e<}KT# zc>EM8D=!wmPZtnRUU=kha>#;tGH&t&NkJ-nz`1Kjvut>Mg`9Bq=^#AzL`f42jYwb9 zHmRL9S;FPz%EZtkyF_Bmc9GYs4aC!JM!4k2>N6f}S&Tzl7&_F0e2l&X?pWZdL3{wt z)td+w23#T)1Uoc|y>w}IhG8Tdpd-jJXf=0Ez`nMiyi!T<$kxq7)Uvy_@JWKiyZfbU zL!+(Xf!w6yXG&m9wb*7xJecv76DLAF;k+4m(&>yeuHU4jqjG$$6jxP3Wu>9|yplGt zKT7SA_Lf~TZO%-o0Nz@xIf)q)C6!1@u((K_jwj4M+0n2~^JL8Au_1rT0u=e1D z8@_?{qecRyC9>-IS6~>JI5~y2ZDE0jkJTRgE595(Z;mAUqgbn`?yw=nW7u@sbnG@D zL?f;Z$99;)PV*<1`e4q_Aw6e6jh=_K=YC`tRC)@^91IT6-0HE+i?RKlcTVm7IF2Hx zhs9e_tQCR6ej_dIosouA;Dm;WU%m~`eO5J*C{6bqzdWobj_usppDvwt81}6^xxo<) zEVM{8Ax+Jl($?N5{R43^v7q~*!eZ?mVy#{$6=P~3IZ%I4VcqR*q7_$3+f#2y5~|Md z7!ocDNKH+tjIAz{a2Un~t+kLY>!_(!EJ{7 zIvkn9^nyV_nS)+aZJHY@<{Vx0d}?;@^GVAyJ?crH73)sVFI@tjto?>~F`f{9ZHur_$Xn|M4aOJCb4&po|DRzA5JC(zm5ASp)R zDotHeOE*cRWrqlD`6yf)9D$O+U1TNPk)N9*C!czhe0;$S@!>gW&GhkFNx&cIjwGAP zYD<4AvK*xr!fdXGufUa(-$8vdSr%dJlAbeU6-n>Qv-A?B(v+0vnQm@%soEMKDMzvQ~rmic4>cp@=ZAy zl}aat#elTnk%Wsv@FW$}7-x2{+>D*QVok!M8LT}9=mv9qupG>GtTh?w-##E)kb34R zhs#A@7;jIl@${rj?RB3|zqI_U#~Rh+bsR|zR?Mm}#%IGQSOfKTv7_2J!6{uM4uxcn zx?76|?2R((nF{fI28WS}vY-*@E3@vK*1GoVn}2bNe|q=PY0Y7bJ35y^jNg9Ip;x~6 z^-_?V-&BPoU(Uq(LzP%zYB>ATVd9gLRN`ySMVeQ{L4J9~?*Cnz*H* zaL*I77J(W*B8J}Ld9>gpn5sW7oR^)6nKLSa6O;Xk#Z{AohNi>djV1TU7_0%9zA&Ln zMRqjpkb7^xUAFJsA!%H{F>o@<{Cq%0E4+|7v?GZ~>cKO|2Bx5F#_7lKYHBirF)NWV z6)x-)Q0c=9|FdzL zxzcpfN<%C0@k?H?vGWgx!?Ja2k9-Ox&RezuHq{C_1L42mVZt*5Ju>Z}a*0QxPy7hC+KUy>( zr=cG#o=sP#=ghf88u)C)ot?-N>#Uma7)#G=!17)W5dsU9{#UCw7cs76JWclC>?{MT zYt6PkIsLS`^6jh6m5|SrzP^6(1p_#qNl0l)iNw=}+;RJ3^3bCV^08^9>hLs!vp1e@ ztIa2AoTW5FIK~hk0=>HBJ>)NKk#?l1e!(ZWi~rFu3SNc#4E9t@kiMriN>%!xKLwK< zo_}E0$xQJC{Wu*~*E>Mktq5(Bg0qf;H|y|2XWLjQG-*O&IjjXAC%Q7s zz!Xx?#PLeJJC2(**}cuRb?3^dpPMH|;h@AKcsw4BOJ7SXP7TV$F0GV`iX!>WHRs8k zxzpq~*FCKEN8A$yD{x>M#A$(=8a#hhTF*y;z{L|>YT$|`?REH}8wSe408iaDXIS>g zMA@}ML3TFzRmO2<@x@2Wru*H!kkbiwmTISu^PPoj8RvokAJ;tk z_*7HeN6{;u5|)WGr^vjI&y=H&I#?!8tj41N=s!@@z_ARaD!WYZ*^kNRF*q#%BVcN8 z-`OFrzP3qLzP>@4);CIjV?>F9e|#`yn^rS8v;unXp~qTNkFY;=m^g6RHINDKO3pk; z%?h~BL2{l(dUmX;$jJQ^j5$lu;S(_lf1!{2NvY=fp~u$K<7}_D+V*Df)Wx&Rh&*|L zx%~@y%xN{3-67UpwlG|GcYSEpqgUTMc1-E%IBCw{7{^l-%5E%EIOrr`9Z|@kK}inI1t=EL zXv&^W#HRx<+NYxD&6dBh=?C?kqobF8%^{Tmxq*Kw7N z%gZ7=tlewXnxC+W4Vcw8fbOSVhQZYjZQWL;EPfjP_Y!4T#1qMQu@4X9psK)uAq-Jf zu~@hi%B8HLrXy7@!}us0vtIl#DB|%{jP)BDF7ec5kLz;P7_)W*NasOLB9}gb+jKTI zLcaxL&SG>Jrm{SmoV>YG^O_a5$G~QV_s+Q2F<#LJ%me zqX{>49!DZ|I!TvZNDx>EM*tbIN!gtSB)XYSNynCrunWTyg!!yvF~QqelIZW3bUY%J zQw~y>KZdMFL<)3}x9mEzkO}VzZoiHKF22}HaMz)1Yy^J`#vD-C=DIOe8WUJiUPYg$ zff8VtuJbK`3LLET!895`g4fdjCSLtge6ZnH&e*^>szI?EipOQ=Ic`_wV-5-K!PC)j z^;l_sdxb<=R|9JrQsa}Nv4_fpxeJi*N7_(rg=*j`9%I`Yx&WwGzkB;(e~5%nC8lFi z{Muo*>fhcfe$c#vF^3((lA$_2UxJT!*yfkGT5wkh5r9rmPaJum!U1#O_u!iYr((cA z0+ZfETv2oW{7ck{ucd>CkO*vgAVfpyLBfYCE9H=rJ}b%SacXnw!TRJcDgqjq)V#Zp z13V>w*^DcSxP)t3YR$>X-cwL92gbms$P}zuOPqqci}Ri^>|;6`EF>tZtVqS5X%-vL zC2gYzbC`)a>0uh(ZW1KS|5zhuG`>~HRXe>*193-GCVKGe)n zl7T^WK~H}LgCV^JX@>A1kTBqmT_Ku@XPFkldRWuL(O7Ey)XJ*bPxj-w(_*GvD`mPq=QHJV*j zZ9ieek65{_|B~5$f8XV%`9?k3=c&d0(wIwhPB6UKrGF3pagd&i;p0i30#1>FKwcsh zpe43rfREbfGcSl>& za*Z@*9+x<-lIn7r7W(Vo%=!O{Jz4}034M!XEtg1iL!m4Kb7(p*26N7b&;D~91%rYb z1UbFtKuG%=p$<+0gLaz~oPJdx8yRyB&J#avmB_b^cC7@Ork@KiFlbRrtSEM~)#Xa| zGS9b2=J`!{YmGD_GjSIUnr4H~l9{P3s!2^<{Xh`cKeTcf?tx;lI`Jw&3%Y||^x}#& zNNs<% zQSJ28(lhf_>E~j9)`_^B0zZx2@uBdqg1ZZD9ULchL8S)x6_MzV1}JI4De+a9eYm6c z?qW(cM5)2ufB7Y@B1bTl@6&o@DxR~of?>?#d!auyTBQ+htp%Y`u(U&MVc~DX<-lEV z-N1tX+V~=M9jfFT7fD&&_a!j~C;z*^cD##CO9O_6(oywz(du$f6I!;U7I(BM%u})D z`i=Bwx+GotzcT(&WZnRW_jimx!0Si9uVx&;E*Bhj)B6Q%k(!GF(g{NJ!hIf&vrp%4 zq#aB+949#cW>(3s@WSn8WMMg1;>C?fHDYgqF~7w@!NHj1RbWpMn8fGTSo$&P!5BCz zo{=q?b~&gQuZJ|eA^vApq}uxI}A4%&Vgk}so@w3E;a0kn#~N161eU0YZ8yZKsXyyFf|@K zj~!53cUt3R0v^quX>7wR_Q`fooU@0Oc<2V@r6h^5Sx`*pTcvWC9aP^SD8XALO68%> zy5fu+IzX9=far(t8w0Z5j>)rnM4naOWVPiiyF^Y!{Uu;FZo$dXc9A@Yqq65rP6b;Y zlPPu=wzFT=n*7r>&n;W54AmOd*${P%c)`FWR>2y`gP7|L)j|lWeuWqNA3qp`!%*MS z%rtpM?~|YD9r%ubPf3$opB*JtC}ovkQlmoQPGCchNBJ?hLV|C%;jL{`GO*tILO%IN zTPwKBniLB|roiDG2-9De8_gPE$%biETn&l!U?bawZSwb|GIfGh>N`c=c?Ly3i^>Fc zBP(AjTG*fc!5of=0zt}79HSe{+6{ZcMRrO0o8}nrg`Vwrc5ULLLKb?$hEg9%qNA#&DN&izK~x?@XyL5-J}s zofZW@3_5vc9e(>U zY5a!n+hJJ^JM84AQ7j5K>Wo2~ia_-D2arbI`y}23K{;x% zR~DE^j?yuc&V!S zo#zv?ZTp>Fpu%sTM?wJ7fv@LefxapQAMb#uMF}5w!MR+@ z#x*X>y2dHE-x$O8-QGHV*>B#vL@?y5cu(LqYz&Ko=nnQgn6)O=3i@i`gLek5TNzS< z>sIBZh9tWTIq&NU`Qj;YJWxX^iUr86Mh1Sc_9M>*&Yc;;hDcee=(&ccy$2A3Sg zoQTu6Bske^nJpR1x&$?!fFtY|RhDf!`%9u62tc(#vVvUl8(e7mN0}`*^lJ9SmuqF7 zW2|CWXa-*Bur&i$%_Nk{?Qu>WrTV|)7zH^=>bq(3WOwxpFBHez41BE#x0_M2_ z_NlqoGXD+!ogI4W%B^*DMX2MC}FB(#E0ipI*@9yZ^@t zpB0@So@d!V`MPEA)cWl6@rk0zI^G2~bYm>kW|id`z!RW9ZZcE1qQWLHhQPM>%le{P zANF$~F~{wiD@9bf4U4VFm!RCP1HCUm{B#Hpt|j1Q1?a75>WL`sA$Y+IT%s+deGNEf z2Hs2Cj5yC~F#n^mtoGLMWw)(XreUjQ{~1%^Arx&^6|yRf96ewd!+srz%wfM=DKgGO zfNSlCHK1-YM;i=^tc(h{4_I8_AR^smscF(qU zZRg$3$Qnq~a$~?;1}1M&f`tOOA!ICq57_T2EZdC_=Pz|t-idVK$!b4338JH(w9X!n z?-JpP_MBSv*FtDh`8LmnaG((U_5sg?k#_KokHWDr9_limoTeGda(qff z6P9@_!+e^=Za#tvScmz<%M*OpjAfc@LL(^ExB;pz6ygsK^&sBHLmw*$_Cd!Bh9*c^lta2;@MPMBrC?9^qNjdHE<``}nN zljBT;6L<>R90&xau?~O)An3@u0&W8wapt;hz*&bf&Q_Df1%K+m+X+YfQQ&+;rDlmo zLJbEJgWtvZ6v}ryj#svLAB~{$T_5>>gkv9iobl{Jn5H_xowN)qy1}nNKKrBUz<&j4 zxGJQ;9V6+x!YM<1q$A7!$tmaJ8q$E1hgJLd*K@3mchLSw*)tEV({m~-3ff0?VMfwyPq#0 z?JEdxhkq~p2jKpS@~l?{cM~^&IXZZxM?W=Ak*Mq!-pnAi3FSVNQ0ys8!r<2dd=!n=I&^Y9?3}1>O zRKVe4newT+=D|_o#=zkxRAF7ZU7Q{ zF#I&!9Zng=h#Y(_(KWQ!gmNp~JcKWUBjG=jr>;wjN|u4UnPuQ#%s#;L91jv*)j%CU1^V_Qw0#xQeh>Ey9M?1&0pA9G z9+WP^A8g%%G9<=seP>w!^T@n!pw4*U-HQ4@!ur9#h)4~;Cqm9w&TP} zOu|?vetR}LM5VY)4+0ORhW(-iO6iZn(E{y)L##lwgHuSD%$xA@@yCik@sWNh`vw0wIIcO5I%Rfa z&Ta`g$k+*rTp5s^0&%9<%*JOT{za&vF6X;1?x0u}S*q+rgb9Zuem3eBBW%IZ&g46o z^N|KmUF4+CyW|%GCw2Q9==84;e+u!Ph~r%-Ydna2B7CYV!i8{D8O6Z=8Q`ixnEf~m zajrFMv2nc`c+Lc#ClDumGaipOBcC7eR-0R-%|(0>;+vpq-a{Ir9EGg3ISQU1X_ z)j_*Yz|C~((4+cv+o9yBzM;&G%mn_E99&ABIrvpMr015uf#q|oMz_x?jum*WV?F_l zn&IFa93#sh?cb4233lt|;#d7aKG$JISC)6mK7cgBZi3>@`fPUo-vAn)fh=5);AifY z1mMLnqYr<^WZSSR2Ff0ca_j^d#yioRB|`xDY}bQX#MwwmA`rJ)K;c({AhR&*sfO^U z46Ig2DOE@t9KkJ%KVRTG1o${Gwx^^Mc$*P!hodOZffThMNW%b8-;6xvnNQrzXa9+x zeN^-Tz9xh_;0SIRwKidV_&anq$Fd!H-N?k1x83^V;#c?-7SIOFnF~4&fR67Vjd@C) zIQa*Jm)l_yn1tCAFmG@9x%G&5IPTp1;o;n}!}Az_ul#&4?d}VWS<%U^5-)v#W**CX;EVD1|<>F<3hR2aK1okDL74wbJ#j&@%A&okC r_B5{HWj~64M1hYe@DT+*>=gL_d-r`<6cl&$00000NkvXXu0mjfZzoN6 From bbaed009724376ceaa3e13b9bf1897985671ccc3 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 17:03:24 -0400 Subject: [PATCH 04/25] update(server): remove Redis, clustering. Convert session storage to Mongo. upgrade to latest Express and related libraries convert tasks to be triggered directly instead of via Redis+Clustering convert cron to call new tasks remove deprecated express-annotations remove API docs since they depended upon express-annotations convert rate metering to in-memory only since Redis is no longer an option temporarily disable CSRF protection Fixes #96 #80 --- app/developer/developer.controller.js | 8 +- app/developer/developer.html | 63 +++--- lib/config/env/production.js | 5 - lib/config/express.js | 99 ++++----- lib/controllers/api/documentation.js | 48 ---- lib/controllers/api/index.js | 49 +---- lib/controllers/api/v1/admin.js | 79 ++----- lib/controllers/api/v1/applications.js | 32 +-- lib/cron/fetch_chapters.js | 8 +- lib/cron/fetch_events.js | 4 +- lib/cron/monthly_metrics.js | 6 +- lib/risky.js | 294 ------------------------- lib/routes.js | 8 +- lib/tasks/fetch_chapters.js | 2 +- lib/tasks/fetch_events.js | 22 +- lib/tasks/index.js | 36 ++- lib/tasks/monthly_metrics.js | 2 +- package.json | 26 ++- server.js | 124 +++-------- 19 files changed, 198 insertions(+), 717 deletions(-) delete mode 100755 lib/controllers/api/documentation.js delete mode 100644 lib/risky.js diff --git a/app/developer/developer.controller.js b/app/developer/developer.controller.js index 7ca4f8b..6965622 100644 --- a/app/developer/developer.controller.js +++ b/app/developer/developer.controller.js @@ -2,10 +2,6 @@ angular.module('gdgxHubApp').controller('DeveloperCtrl', DeveloperCtrl); -DeveloperCtrl.$inject = ['$scope', '$http']; +DeveloperCtrl.$inject = []; -function DeveloperCtrl($scope, $http) { - $http.get('/api/v1/rest').success(function(data) { - $scope.restDiscovery = data; - }); -} +function DeveloperCtrl() {} diff --git a/app/developer/developer.html b/app/developer/developer.html index d6d96a3..a65850d 100644 --- a/app/developer/developer.html +++ b/app/developer/developer.html @@ -1,38 +1,33 @@ diff --git a/lib/config/env/production.js b/lib/config/env/production.js index 637d2da..2b3b7fe 100755 --- a/lib/config/env/production.js +++ b/lib/config/env/production.js @@ -9,10 +9,5 @@ module.exports = { sender: 'GDG[x] Hub ', transport: 'Direct', error_recipient: process.env.ADMIN_EMAIL // jshint ignore:line - }, - redis: { - host: process.env.REDIS_DB_HOST, - port: process.env.REDIS_DB_PORT, - password: process.env.REDIS_DB_PASSWORD } }; diff --git a/lib/config/express.js b/lib/config/express.js index 999899c..8906900 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -3,37 +3,23 @@ var express = require('express'), cookieParser = require('cookie-parser'), session = require('express-session'), + methodOverride = require('method-override'), + favicon = require('serve-favicon'), path = require('path'), passport = require('passport'), - csrf = require('csrf'), - config = require('./config'), - redis = require('redis'), - RedisStore = require('connect-redis')(session); + csrf = require('csurf'), + config = require('./config'); +var errorhandler = require('errorhandler'); +var morgan = require('morgan'); +var compression = require('compression'); +var bodyParser = require('body-parser'); +const MongoStore = require('connect-mongo')(session); /** * Express configuration */ module.exports = function (app) { - - var redisClient; - - if (config.redis) { - console.log('Using redis for Express...'); - redisClient = redis.createClient(config.redis.port, config.redis.host); - - if (config.redis.password) { - redisClient.auth(config.redis.password); - } - - redisClient.on('ready', function () { - console.log('Redis is ready for use with Express.'); - }); - redisClient.on('error', function (err) { - console.error('ERR:REDIS:Express: ' + err); - }); - } - - app.configure('development', function () { + if (process.env.NODE_ENV !== 'production') { app.use(require('connect-livereload')()); // Disable caching of scripts for easier testing @@ -48,46 +34,43 @@ module.exports = function (app) { app.use(express.static(path.join(config.root, '.tmp'))); app.use(express.static(path.join(config.root, 'app'))); - app.use(express.errorHandler()); + app.use(errorhandler()); app.set('views', config.root + '/app/views'); - }); + app.use(session({ + cookie: { secure: false }, + resave: false, + saveUninitialized: true, + store: new MongoStore({ url: process.env.MONGODB_DB_URL }), + secret: config.sessionSecret + })); + } - app.configure('production', function () { - app.use(express.favicon(path.join(config.root, 'public', 'favicon.ico'))); - app.use(express.compress()); + if (process.env.NODE_ENV === 'production') { + app.use(favicon(path.join(config.root, 'public', 'favicon.ico'), {})); + app.use(compression()); app.use(express.static(path.join(config.root, 'public'), {maxAge: 604800000})); app.set('views', config.root + '/views'); - }); - app.configure(function () { - app.engine('html', require('ejs').renderFile); - app.set('view engine', 'html'); - app.use(express.logger('[' + process.pid + '] :method :url :status :response-time ms - :res[content-length]')); - app.use(express.json()); - app.use(express.urlencoded()); - app.use(express.methodOverride()); - app.use(cookieParser()); - app.use(csrf({cookie: true})); - - if (config.redis) { - app.use(express.session({ - store: new RedisStore({ - client: redisClient - }), - secret: config.sessionSecret - })); - } else { - app.use(express.session({ - secret: config.sessionSecret - })); - } + app.use(session({ + cookie: { secure: true }, + resave: false, + saveUninitialized: true, + store: new MongoStore({ url: process.env.MONGODB_DB_URL }), + secret: config.sessionSecret + })); + } - // Passport - app.use(passport.initialize()); - app.use(passport.session()); + app.engine('html', require('ejs').renderFile); + app.set('view engine', 'html'); + app.use(morgan('[' + process.pid + '] :method :url :status :response-time ms - :res[content-length]')); + app.use(bodyParser.json({})); + app.use(bodyParser.urlencoded({ extended: false})); + app.use(methodOverride()); + app.use(cookieParser()); + // app.use(csrf({cookie: true})); - // Router needs to be last - app.use(app.router); - }); + // Passport + app.use(passport.initialize()); + app.use(passport.session()); }; diff --git a/lib/controllers/api/documentation.js b/lib/controllers/api/documentation.js deleted file mode 100755 index 2ee68fe..0000000 --- a/lib/controllers/api/documentation.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -var methods = require('methods'); - -module.exports = function (version, app, impl) { - - var meta = impl.get('metadata'); - - if (meta) { - app.get('/api/' + version + '/rest', function (req, res) { - var response = { - kind: 'discovery#restDescription', - version: version, - name: meta.name, - title: meta.title, - description: meta.description, - ownerDomain: 'hub.gdgx.io', - baseUrl: 'https://hub.gdgx.io/api/v1/', - status: meta.status, - ownerName: meta.ownerName, - resources: { - url: { - methods: {} - } - } - }; - - methods.forEach(function (method) { - if (impl.routes[method]) { - impl.routes[method].forEach(function (method) { - var info = { - id: meta.name + '.' + method.path.replace('/', '').replace(/\//g, '.').replace(/:/g, ''), - path: method.path, - method: method.method - }; - - if (impl.annotations[method.method + '-' + method.path]) { - info.doc = impl.annotations[method.method + '-' + method.path]; - } - - response.resources.url.methods[method.path.replace('/', '').replace(/\//g, '.').replace(/:/g, '')] = info; - }); - } - }); - res.json(response); - }); - } -}; diff --git a/lib/controllers/api/index.js b/lib/controllers/api/index.js index 32bb244..dd173f3 100755 --- a/lib/controllers/api/index.js +++ b/lib/controllers/api/index.js @@ -1,57 +1,27 @@ 'use strict'; var express = require('express'), - documentation = require('./documentation.js'), - redis = require('redis'), rate = require('express-rate'), config = require('../../config/config'), mongoose = require('mongoose'), SimpleApiKey = mongoose.model('SimpleApiKey'), - annotations = require('express-annotations'), Cacher = require('cacher'), uuid = require('node-uuid'), utils = require('./utils'), - request = require('superagent'), - CacherRedis = require('cacher-redis'); + request = require('superagent'); module.exports = function (app) { const EDGE_CACHE_MAX_AGE = 3600; // 1 hr var versions = []; var rateHandler; var cacher; - var redisClient; utils.fixCacher(Cacher); - if (config.redis) { - console.log('Using redis for API support...'); - - // Setup Redis client for API Rate limiting - redisClient = redis.createClient(config.redis.port, config.redis.host); - - if (config.redis.password) { - redisClient.auth(config.redis.password); - } - - redisClient.on('ready', function () { - console.log('Redis Client for API support is ready.'); - }); - redisClient.on('error', function (err) { - console.error('ERR:REDIS:Cacher: ' + err); - }); - - rateHandler = new rate.Redis.RedisRateHandler({client: redisClient}); - - cacher = new Cacher(new CacherRedis(redisClient)); - } else { - console.log('Using in-memory'); - - // Fallback to In-Memory handler if Redis is not available - rateHandler = new rate.Memory.MemoryRateHandler(); - - // In-Memory Cache - cacher = new Cacher(); - } + console.log('Using in-memory rate handler...'); + rateHandler = new rate.Memory.MemoryRateHandler(); + // In-Memory Cache + cacher = new Cacher(); var analyticsMiddleware = function (version) { return function (req, res, next) { @@ -151,20 +121,15 @@ module.exports = function (app) { versions.push(version); var impl = express(); - annotations.extend(impl); impl.use(apiKeyMiddleware); impl.use(rateMiddleware); impl.use(analyticsMiddleware(version)); impl.use(edgeCache); - impl.route = function (method, path, metadata) { + impl.route = function (method, path) { var args = Array.prototype.slice.call(arguments); - if (metadata) { - impl.annotate(method + '-' + path, metadata); - } - impl[method](path, args.slice(3)); }; @@ -174,8 +139,6 @@ module.exports = function (app) { require('./' + file)(impl, cacher); - documentation(version, app, impl); - app.get('/api/', function (req, res) { res.redirect('/developers/api'); }); diff --git a/lib/controllers/api/v1/admin.js b/lib/controllers/api/v1/admin.js index 9bc7bac..3dea1a0 100644 --- a/lib/controllers/api/v1/admin.js +++ b/lib/controllers/api/v1/admin.js @@ -1,69 +1,18 @@ 'use strict'; -var redis = require('redis'), - config = require('../../../config/config'), - middleware = require('../../../middleware'), - risky = require('../../../risky'); - -module.exports = function (app) { - var redisClient = null; - - if (config.redis) { - console.log('Using redis for API Rate Limiting...'); - // Setup Redis client for API Rate limiting - redisClient = redis.createClient(config.redis.port, config.redis.host); - - if (config.redis.password) { - redisClient.auth(config.redis.password); - } - redisClient.on('ready', function () { - console.log('Redis is ready for use in API Rate Limiting.'); +var middleware = require('../../../middleware'); +var tasks = require('../../../tasks'); + +module.exports = function(app) { + app.route('post', '/admin/tasks', + middleware.auth({roles: ['admin']}), function(req, res) { + var status = tasks(req.body.task_type); + if (status === 200) { + res.jsonp({msg: 'Running task ' + req.body.task_type}); + } else if (status === 404) { + return res.send(404, 'Unknown task \"' + req.body.task_type + '/".'); + } else if (status === 500) { + return res.send(500, 'Server failure when running task \"' + req.body.task_type + '/".'); + } }); - redisClient.on('error', function (err) { - console.error('ERR:REDIS:Admin: ' + err); - }); - } - - app.route('post', '/admin/tasks', {summary: '-'}, - middleware.auth({roles: ['admin']}), function (req, res) { - // jshint -W106 - risky.sendTask(req.body.task_type, req.body.params, - function (err, id) { - if (err) { - return res.send(500, 'Not executing task.'); - } - - res.jsonp({msg: 'task_runs', taskId: id}); - }, - null, true); - // jshint +W106 - }); - - app.route('post', '/admin/tasks/cluster', { - summary: '-' - }, middleware.auth({roles: ['admin']}), function (req, res) { - res.jsonp(risky.getCluster()); - }); - - app.route('post', '/admin/cache/flush', { - summary: '-' - }, middleware.auth({roles: ['admin']}), function (req, res) { - if (redisClient) { - redisClient.keys('cacher:*', function (err, replies) { - if (replies.length === 0) { - res.jsonp({msg: 'nothing to flush', code: 404}); - } else { - redisClient.del(replies, function (err) { - if (err) { - res.jsonp({msg: 'flush failed', code: 500}); - } else { - res.jsonp({msg: 'flushed express cache', count: replies.length, code: 200}); - } - }); - } - }); - } else { - res.jsonp({msg: 'not connected to redis', code: 500}); - } - }); }; diff --git a/lib/controllers/api/v1/applications.js b/lib/controllers/api/v1/applications.js index 8f84499..9fa5525 100644 --- a/lib/controllers/api/v1/applications.js +++ b/lib/controllers/api/v1/applications.js @@ -9,9 +9,7 @@ var mongoose = require('mongoose'), module.exports = function (app) { - app.route('post', '/applications', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('post', '/applications', middleware.auth(), function (req, res) { var application = new Application(req.body); application.users.push({_id: req.user._id, level: 'owner'}); @@ -30,17 +28,13 @@ module.exports = function (app) { }); }); - app.route('get', '/applications', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('get', '/applications', middleware.auth(), function (req, res) { utils.getModel('Application', { user: req.user._id })(req, res); }); - app.route('get', '/applications/:applicationId', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('get', '/applications/:applicationId', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, user: req.user._id}, function (err, application) { if (err) { @@ -53,9 +47,7 @@ module.exports = function (app) { }); }); - app.route('delete', '/applications/:applicationId', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('delete', '/applications/:applicationId', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, 'users._id': req.user._id}, function (err, application) { if (err) { @@ -70,9 +62,7 @@ module.exports = function (app) { }); }); - app.route('post', '/applications/:applicationId/simpleapikeys', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('post', '/applications/:applicationId/simpleapikeys', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, 'users._id': req.user._id}, function (err, application) { if (err) { @@ -97,9 +87,7 @@ module.exports = function (app) { }); }); - app.route('get', '/applications/:applicationId/simpleapikeys', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('get', '/applications/:applicationId/simpleapikeys', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, 'users._id': req.user._id}, function (err, application) { if (err) { @@ -114,9 +102,7 @@ module.exports = function (app) { }); }); - app.route('get', '/applications/:applicationId/consumers', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('get', '/applications/:applicationId/consumers', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, 'users._id': req.user._id}, function (err, application) { if (err) { @@ -131,9 +117,7 @@ module.exports = function (app) { }); }); - app.route('post', '/applications/:applicationId/consumers', { - summary: '-' - }, middleware.auth(), function (req, res) { + app.route('post', '/applications/:applicationId/consumers', middleware.auth(), function (req, res) { Application.findOne({_id: req.params.applicationId, 'users._id': req.user._id}, function (err, application) { if (err) { diff --git a/lib/cron/fetch_chapters.js b/lib/cron/fetch_chapters.js index f7c1a16..4a8649f 100755 --- a/lib/cron/fetch_chapters.js +++ b/lib/cron/fetch_chapters.js @@ -1,7 +1,7 @@ 'use strict'; var CronJob = require('cron').CronJob, - risky = require('../risky'); + tasks = require('../tasks'); -module.exports = new CronJob('0 */5 * * *', function(){ - risky.sendTask('fetch_chapters'); - }); +module.exports = new CronJob('0 */5 * * *', function() { + tasks('fetch_chapters'); +}); diff --git a/lib/cron/fetch_events.js b/lib/cron/fetch_events.js index 56a707a..5c0ceb3 100755 --- a/lib/cron/fetch_events.js +++ b/lib/cron/fetch_events.js @@ -1,11 +1,11 @@ 'use strict'; var CronJob = require('cron').CronJob, moment = require('moment'), - risky = require('../risky'); + tasks = require('../tasks'); module.exports = new CronJob('0 */5 * * *', function() { // Ask to fetch events for the current month and year. // The fetch_events task will actually try to fetch events starting at month - 1 and ending with month + 6. // See ../tasks/fetch_events.js for more details. - risky.sendTask('fetch_events', { month: moment().month(), year: moment().year() }); + tasks('fetch_events', {month: moment().month(), year: moment().year()}); }); diff --git a/lib/cron/monthly_metrics.js b/lib/cron/monthly_metrics.js index a745fd1..8507602 100644 --- a/lib/cron/monthly_metrics.js +++ b/lib/cron/monthly_metrics.js @@ -1,8 +1,8 @@ 'use strict'; var CronJob = require('cron').CronJob, - risky = require('../risky'); + tasks = require('../tasks'); -module.exports = new CronJob('0 0 1 * *', function(){ - risky.sendTask('monthly_metrics'); +module.exports = new CronJob('0 0 1 * *', function() { + tasks('monthly_metrics'); }); diff --git a/lib/risky.js b/lib/risky.js deleted file mode 100644 index fa2ec24..0000000 --- a/lib/risky.js +++ /dev/null @@ -1,294 +0,0 @@ -'use strict'; - -var redis = require('redis'), - mongoose = require('mongoose'), - TaskLog = mongoose.model('TaskLog'), - uuid = require('node-uuid'), - moment = require('moment'); - -module.exports = (function () { - var receiver, emitter, prefix; - var readyCount = 0; - var myId; - var startup; - var self; - var master = false; - var cluster = {}; - var heartbeatInterval; - - var taskHandler = {}; - var tasks = {}; - - var evaluateMaster = function () { - var iAmMaster = true; - for (var prop in cluster) { - if (cluster[prop].started < startup) { - iAmMaster = false; - } - } - - console.log('was master: ' + master + ', now master: ' + iAmMaster); - master = iAmMaster; - }; - - var taskDone = function (id, type, started, executor, err, result, cb) { - var ended = moment().valueOf(); - var elapsed = ended - started; - - var logentry = new TaskLog(); - logentry._id = id; - logentry.task_type = type; // jshint ignore:line - logentry.started_at = moment(started); // jshint ignore:line - logentry.ended_at = ended; // jshint ignore:line - logentry.requested_by = myId; // jshint ignore:line - logentry.executed_by = executor; // jshint ignore:line - - if (err) { - logentry.result = 1; - logentry.msg = JSON.stringify(err); - } else { - logentry.result = 0; - } - logentry.save(); - - if (tasks[id]) { - delete tasks[id]; - } - - cb(err, result, elapsed); - }; - - var responder; - - var ready = function () { - readyCount++; - if (readyCount === 2) { - startup = moment().valueOf(); - console.log('Risky is up. I\'m ' + myId); - self.on('group:hello', function (data) { - if (data.sender !== myId && data.type === 'hi') { - if (responder) { - console.log('Cancel masterResponder'); - clearTimeout(responder); - responder = null; - } - - console.log(data.sender + ' just said hi. Replying.'); - cluster[data.sender] = { - started: data.started, - nextHeartbeat: moment().valueOf() + 10000 - }; - evaluateMaster(); - - self.emit('group:hello', { - sender: myId, - type: 'hi_reply', - started: startup - }); - } else if (data.sender !== myId && data.type === 'hi_reply') { - if (responder) { - console.log('Cancel masterResponder'); - clearTimeout(responder); - responder = null; - } - - cluster[data.sender] = { - started: data.started, - nextHeartbeat: moment().valueOf() + 10000 - }; - evaluateMaster(); - } - }); - - self.on('group:heartbeat', function (data) { - if (data.sender !== myId && cluster[data.sender]) { - cluster[data.sender].nextHeartbeat = data.nextHeartbeat; - if (cluster[data.sender].timeout) { - clearTimeout(cluster[data.sender].timeout); - cluster[data.sender].timeout = null; - } - var next = (data.nextHeartbeat - moment().valueOf()) + 2000; - - cluster[data.sender].timeout = setTimeout(function () { - console.log(data.sender + ' has gone down...'); - delete cluster[data.sender]; - evaluateMaster(); - }, next); - } - }); - - self.on('group:taskrequest', function (data) { - if (!master) { - if (taskHandler[data.type]) { - // Offer to execute task - console.log('Offering to execute task with type: ' + data.type); - self.emit('group:taskoffer', { - sender: myId, - type: data.type, - id: data.taskId - }); - } else { - console.log('Don\'t know how to execute task'); - } - } else { - console.log('I\'m the master, not doing any tasks'); - } - }); - - self.on('group:taskoffer', function (data) { - // First one to send an offer wins - if (tasks[data.id] && tasks[data.id].state === 'open') { - tasks[data.id].state = 'executing'; - tasks[data.id].executor = data.sender; - - tasks[data.id].acceptCallback(null, data.id, data.type, moment()); - - self.emit('group:taskstart', { - sender: myId, - type: data.type, - recipient: data.sender, - params: tasks[data.id].params, - id: data.id - }); - } - }); - - self.on('group:taskstart', function (data) { - if (data.recipient === myId) { - console.log('Executing task... type: ' + data.type + ', id: ' + data.id); - taskHandler[data.type](data.id, data.params, function (err, result) { - self.emit('group:taskdone', { - sender: myId, - type: data.type, - recipient: data.sender, - err: err, - result: result, - id: data.id - }); - }); - } - }); - - self.on('group:taskdone', function (data) { - if (tasks[data.id] && tasks[data.id].executor === data.sender) { - // Task got executed - taskDone(data.id, data.type, tasks[data.id].started, tasks[data.id].executor, data.err, - data.result, tasks[data.id].cb); - } - }); - - self.emit('group:hello', { - sender: myId, - type: 'hi', - started: startup - }); - responder = setTimeout(function () { - evaluateMaster(); - }, 4000); - - var hb = function () { - var next = moment().valueOf() + 5000; - self.emit('group:heartbeat', { - sender: myId, - nextHeartbeat: next - }); - }; - heartbeatInterval = setInterval(hb, 5000); - hb(); - } - }; - - self = { - connect: function (options) { - options = options || {}; - var port = options && options.port || 6379; // 6379 is Redis' default - var host = options && options.host || '127.0.0.1'; - var auth = options && options.auth; - - myId = options && options.id; - - emitter = redis.createClient(port, host); - receiver = redis.createClient(port, host); - - emitter.on('error', function (err) { - console.error('ERR:REDIS:Emitter: ' + err); - }); - receiver.on('error', function (err) { - console.error('ERR:REDIS:Receiver: ' + err); - }); - - emitter.on('ready', ready); - receiver.on('ready', ready); - - if (auth) { - console.log('Authorizing with Redis...'); - emitter.auth(auth); - receiver.auth(auth); - console.log('Redis Auth complete.'); - } - - receiver.setMaxListeners(0); - prefix = options.scope ? options.scope + ':' : ''; - }, - getRedisClient: function () { - return emitter; - }, - getCluster: function () { - return cluster; - }, - setTaskHandler: function (taskType, handler) { - taskHandler[taskType] = handler; - }, - sendTask: function (taskType, params, acceptCallback, cb, force) { - cb = cb || function () {}; - acceptCallback = acceptCallback || function () {}; - - params = params || {}; - force = force !== undefined ? force : false; - - var id = uuid.v4(); - if ((master || force) && Object.keys(cluster).length > 0) { - console.log('Sending out task ' + taskType + ' with id ' + id); - tasks[id] = { - type: taskType, - started: moment().valueOf(), - state: 'open', - params: params, - acceptCallback: acceptCallback, - cb: cb - }; - - self.emit('group:taskrequest', { - type: taskType, - from: myId, - taskId: id - }); - } else if (Object.keys(cluster).length === 0 && taskHandler[taskType]) { - console.log('Doing task myself...'); - acceptCallback(null, id, taskType, moment()); - var started = moment().valueOf(); - taskHandler[taskType](id, params, function (err, result) { - taskDone(id, taskType, started, myId, err, result, cb); - }); - } else { - console.log('Not doing it...'); - acceptCallback('Not doing it'); - } - }, - on: function (channel, handler, cb) { - var callback = cb || function () {}; - - receiver.on('pmessage', function (pattern, _channel, message) { - if (prefix + channel === pattern) { - handler(JSON.parse(message)); - } - }); - - receiver.psubscribe(prefix + channel, callback); - }, - emit: function (channel, message) { - emitter.publish(prefix + channel, JSON.stringify(message)); - } - }; - return self; -})(); diff --git a/lib/routes.js b/lib/routes.js index dc4117e..351909d 100755 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,8 +1,8 @@ 'use strict'; var api = require('./controllers/api'), - index = require('./controllers'), - auth = require('./controllers/auth'); + index = require('./controllers'), + auth = require('./controllers/auth'); /** * Application routes @@ -19,10 +19,10 @@ module.exports = function(app) { app.get('/directives/*', index.partials); app.get('/*', function(req, res, next) { - if(req.url.indexOf('api/') !== -1) { + if (req.url.indexOf('api/') !== -1) { next(); } else { - index.index(req,res,next); + index.index(req, res, next); } }); }; diff --git a/lib/tasks/fetch_chapters.js b/lib/tasks/fetch_chapters.js index b036550..4bc3c47 100644 --- a/lib/tasks/fetch_chapters.js +++ b/lib/tasks/fetch_chapters.js @@ -2,7 +2,7 @@ var async = require('async'), devsite = require('../clients/devsite'); -module.exports = function(id, params, cb) { +module.exports = function(options, cb) { devsite.fetchChapters(function(err, chapters) { async.each(chapters, function(chapter, chapterCallback) { chapter.save(function(err) { diff --git a/lib/tasks/fetch_events.js b/lib/tasks/fetch_events.js index 9f9bd56..557feab 100644 --- a/lib/tasks/fetch_events.js +++ b/lib/tasks/fetch_events.js @@ -11,18 +11,18 @@ var mongoose = require('mongoose'), require('superagent-retry')(request); -module.exports = function (id, params, cb) { - params = params || {}; - var month = params.month > 0 ? params.month - 1 : moment().month(); - var year = params.year || moment().year(); +module.exports = function (options, cb) { + options = options || {}; + var month = options.month > 0 ? options.month - 1 : moment().month(); + var year = options.year || moment().year(); - console.log('[task ' + id + '] asked to fetch events starting with month="' + month + '" year="' + year + '".'); + console.log('Asked to fetch events starting with month="' + month + '" year="' + year + '".'); var firstDayOfMonth = moment().year(year).month(month).date(1).subtract(1, 'months') .seconds(0).minutes(0).hours(0).unix(); var lastDayOfMonth = moment().year(year).month(month).add(6, 'months').seconds(0).minutes(0).hours(0).unix(); - console.log('[task ' + id + '] fetching events: start: "' + moment.unix(firstDayOfMonth).format('MMM DD YYYY') + + console.log('Fetching events: start: "' + moment.unix(firstDayOfMonth).format('MMM DD YYYY') + '", end: "' + moment.unix(lastDayOfMonth).format('MMM DD YYYY') + '"...'); async.series([ @@ -54,7 +54,7 @@ module.exports = function (id, params, cb) { eventsCallback(err); }); }, function (err) { - console.log('[task ' + id + '] saved ' + events.length + ' events'); + console.log('Saved ' + events.length + ' events'); chapterCallback(err); }); } @@ -64,13 +64,13 @@ module.exports = function (id, params, cb) { } }); }, function (err) { - console.log('[task ' + id + '] fetched_events'); + console.log('Done fetching events.'); callback(err, 'done'); }); }); }, function (callback) { - console.log('[task ' + id + '] fetching tags for events'); + console.log('Fetching tags for events...'); var processTag = function (tag, tagCallback, err, events) { events = events || []; async.each(events, function (ev, evCallback) { @@ -121,13 +121,13 @@ module.exports = function (id, params, cb) { } }); }, function (err) { - console.log('[task ' + id + '] done fetching tags'); + console.log('Done fetching tags.'); callback(err, 'two'); }); }); } ], function (err) { - console.log('[task ' + id + '] done'); + console.log('Fetch events task complete.'); if (err) { console.log(err); } diff --git a/lib/tasks/index.js b/lib/tasks/index.js index c4f684d..5fa6b55 100644 --- a/lib/tasks/index.js +++ b/lib/tasks/index.js @@ -1,13 +1,31 @@ 'use strict'; -var risky = require('../risky.js'); +var fetchChapters = require('./fetch_chapters'); +var fetchEvents = require('./fetch_events'); +var monthlyMetrics = require('./monthly_metrics'); -module.exports = function () { - require('fs').readdirSync(__dirname + '/').forEach(function (file) { - if (file.match(/.+\.js/g) !== null && file !== 'index.js') { - var taskName = file.replace('.js', ''); - console.log('Setting Task Handler for ' + taskName); - risky.setTaskHandler(taskName, require('./' + file)); - } - }); +module.exports = function (name, options) { + switch (name) { + case 'fetch_chapters': + fetchChapters(options, function(error) { + console.error('Failed to fetch chapters: ' + JSON.stringify(error)); + return 500; + }); + break; + case 'fetch_events': + fetchEvents(options, function(error) { + console.error('Failed to fetch events: ' + JSON.stringify(error)); + return 500; + }); + break; + case 'monthly_metrics': + monthlyMetrics(options, function(error) { + console.error('Failed to compute monthly metrics: ' + JSON.stringify(error)); + return 500; + }); + break; + default: + return 404; + } + return 200; }; diff --git a/lib/tasks/monthly_metrics.js b/lib/tasks/monthly_metrics.js index 9ff3db5..a815953 100644 --- a/lib/tasks/monthly_metrics.js +++ b/lib/tasks/monthly_metrics.js @@ -7,7 +7,7 @@ var mongoose = require('mongoose'), async = require('async'), utils = require('../utils'); -module.exports = function (id, params, cb) { +module.exports = function (options, cb) { async.series([ function (callback) { // Count all chapters diff --git a/package.json b/package.json index 08149df..75a749b 100644 --- a/package.json +++ b/package.json @@ -19,45 +19,47 @@ "version": "0.2.1", "dependencies": { "async": "1.4.2", + "body-parser": "1.17.1", "burrito": "0.2.12", "cacher": "1.0.0", - "cacher-redis": "0.0.1", "cheerio": "0.19.0", - "connect-redis": "3.0.1", + "compression": "1.6.2", + "connect-mongo": "1.3.2", + "cookie-parser": "1.4.3", "cron": "1.0.9", "csurf": "1.8.3", "dotenv": "4.0.0", "ejs": "0.8.4", - "express": "3.21.2", - "express-annotations": "0.1.0", + "errorhandler": "1.5.0", + "express": "4.15.2", "express-rate": "0.0.1", - "express-session": "1.12.1", + "express-session": "1.15.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", "lodash": "2.4.1", + "method-override": "2.3.7", "methods": "1.1.2", - "mongoose": "3.5.5", "moment": "2.10.6", - "newrelic": "1.5.0", + "mongoose": "3.5.5", + "morgan": "1.8.1", "node-forge": "0.6.34", "node-geocoder": "2.2.0", "node-uuid": "1.4.3", "nodemailer": "1.4.0", - "passport": "0.3.0", + "passport": "0.3.2", "passport-http-bearer": "1.0.1", "passport-http-oauth": "0.1.3", "passport-localapikey": "0.0.3", - "phantomjs": "1.9.18", - "redis": "2.4.2", + "serve-favicon": "2.4.1", "slugify": "0.1.1", "socket.io": "0.9.17", - "superagent-retry": "0.3.0", "superagent": "0.17.0", + "superagent-retry": "0.3.0", "timequeue": "0.2.2" }, "devDependencies": { "bower": "1.7.9", - "connect-livereload": "0.3.0", + "connect-livereload": "0.6.0", "grunt": "0.4.5", "grunt-autoprefixer": "0.4.0", "grunt-bower-install": "0.7.0", diff --git a/server.js b/server.js index c2db3e4..e7101ac 100755 --- a/server.js +++ b/server.js @@ -12,8 +12,6 @@ if (env && !env.error) { var express = require('express'), path = require('path'), fs = require('fs'), - cluster = require('cluster'), - numCPUs = require('os').cpus().length, mongoose = require('mongoose'); /** @@ -31,106 +29,46 @@ console.log = function () { } }; -function initWorker(worker) { - var listeners; +// Default node environment to development +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; - listeners = worker.process.listeners('exit')[0]; - var exit = listeners[Object.keys(listeners)[0]]; +// Application Config +var config = require('./lib/config/config'); - listeners = worker.process.listeners('disconnect')[0]; - var disconnect = listeners[Object.keys(listeners)[0]]; +// Connect to database +var db = mongoose.connect(config.mongo.uri, config.mongo.options); // jshint ignore:line - worker.process.removeListener('exit', exit); - worker.process.once('exit', function(exitCode, signalCode) { - if (worker.state !== 'disconnected') { - disconnect(); - } - exit(exitCode, signalCode); - }); -} +// Bootstrap models +var modelsPath = path.join(__dirname, 'lib/models'); +fs.readdirSync(modelsPath).forEach(function (file) { + console.log('Loading model...' + file.replace('.js', '')); + require(modelsPath + '/' + file); +}); -if (cluster.isMaster) { - var i; - // Fork workers. - for (i = 0; i < numCPUs; i++) { - setTimeout(function () { - var worker = cluster.fork(); - initWorker(worker); - console.log('Worker started, PID ' + worker.process.pid); - }, (i + 1) * 5000); // jshint ignore:line - } +// Import static data +require('./lib/fixtures')(); - cluster.on('exit', function (deadWorker, code, signal) { - // Restart the worker - var worker = cluster.fork(); - initWorker(worker); +// Passport Configuration +require('./lib/config/passport')(); - // Note the process IDs - var newPID = worker.process.pid; - var oldPID = deadWorker.process.pid; +// Initialize admin task handlers +require('./lib/tasks')(); - // Log the event - console.log('worker ' + oldPID + ' died. Code: ' + code + ', Signal: ' + signal); - console.log('worker ' + newPID + ' born.'); - }); -} else { - // Default node environment to development - process.env.NODE_ENV = process.env.NODE_ENV || 'development'; - - // Application Config - var config = require('./lib/config/config'); - - // Connect to database - var db = mongoose.connect(config.mongo.uri, config.mongo.options); // jshint ignore:line - - // Bootstrap models - var modelsPath = path.join(__dirname, 'lib/models'); - fs.readdirSync(modelsPath).forEach(function (file) { - console.log('Loading model...' + file.replace('.js', '')); - require(modelsPath + '/' + file); - }); - - // Import static data - require('./lib/fixtures')(); - - var risky = require('./lib/risky'); - - // Passport Configuration - require('./lib/config/passport')(); - - if (config.env === 'production' && config.redis) { - var myId = 'workerId'; - if (cluster.isWorker) { - myId = 'worker-' + cluster.worker.process.pid; - } - risky.connect({ - port: config.redis.port, - host: config.redis.host, - auth: config.redis.password, - id: myId, - scope: 'risky' - }); - } +var app = express(); - // Initialize admin task handlers - require('./lib/tasks')(); +// Express settings +require('./lib/config/express')(app); - var app = express(); +// Routing +require('./lib/routes')(app); - // Express settings - require('./lib/config/express')(app); +// Initialize cron jobs +require('./lib/cron')(); - // Routing - require('./lib/routes')(app); +// Start server +app.listen(config.port, config.hostname, function () { + console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); +}); - // Initialize cron jobs - require('./lib/cron')(); - - // Start server - app.listen(config.port, config.hostname, function () { - console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); - }); - - // Expose app - exports = module.exports = app; -} +// Expose app +exports = module.exports = app; From 43c57cc70b2c69370012f5f5e93615d36bdaea85 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 20:43:10 -0400 Subject: [PATCH 05/25] update(server): setup for GAE Flex and Cloud Endpoints. Relates to #100 --- .dockerignore | 16 ++++++ .gitignore | 16 +++--- .nvmrc | 2 +- Gruntfile.js | 3 +- app.yaml | 15 +++++ cron.yaml | 4 ++ lib/config/express.js | 8 +++ openapi.json | 131 ++++++++++++++++++++++++++++++++++++++++++ package.json | 75 ++++++++++++------------ 9 files changed, 223 insertions(+), 47 deletions(-) create mode 100644 .dockerignore create mode 100644 app.yaml create mode 100644 cron.yaml create mode 100644 openapi.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0d1c2a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +test/ + +node_modules/ +.git/ +coverage/ +npm-debug.log + +.dockerignore +Dockerfile + +.gitignore +.gitattributes +.editorconfig +.idea/ +*.md +*.iml diff --git a/.gitignore b/.gitignore index 2981ae5..268d94a 100755 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,17 @@ .DS_Store -node_modules -public -.tmp +/node_modules +/public +/.tmp +/tmp .sass-cache -app/bower_components -heroku +/app/bower_components /views -dist -.idea +/dist +/.idea newrelic_agent.log .c9 mongodb +*.log .env .env-prod +npm-debug.log.* diff --git a/.nvmrc b/.nvmrc index 8ac28bf..6b9255c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -4.6.1 +6.9.2 diff --git a/Gruntfile.js b/Gruntfile.js index c7cffc8..c3124c9 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -282,7 +282,6 @@ module.exports = function (grunt) { src: [ 'package.json', 'server.js', - 'newrelic.js', 'lib/**/*' ] }] @@ -352,7 +351,7 @@ module.exports = function (grunt) { grunt.registerTask('serve', function (target) { if (target === 'dist') { - return grunt.task.run(['build', 'express:prod', 'open', 'express-keepalive']); + return grunt.task.run(['build', 'express:prod', 'express-keepalive']); } grunt.task.run([ diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..80eef9d --- /dev/null +++ b/app.yaml @@ -0,0 +1,15 @@ +runtime: nodejs +env: flex +handlers: +- url: /.* + script: IGNORED +endpoints_api_service: + name: api.gdgx.io + config_id: 2017-03-18r0 +network: + instance_tag: https-server + name: gdg-x +automatic_scaling: + min_num_instances: 1 + max_num_instances: 2 + cool_down_period_sec: 300 diff --git a/cron.yaml b/cron.yaml new file mode 100644 index 0000000..7276ce1 --- /dev/null +++ b/cron.yaml @@ -0,0 +1,4 @@ +cron: +- description: daily 9am EST ingestion job + url: / + schedule: every day 13:00 diff --git a/lib/config/express.js b/lib/config/express.js index 8906900..b57158f 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -13,6 +13,7 @@ var errorhandler = require('errorhandler'); var morgan = require('morgan'); var compression = require('compression'); var bodyParser = require('body-parser'); +var yesHttps = require('yes-https'); const MongoStore = require('connect-mongo')(session); /** @@ -73,4 +74,11 @@ module.exports = function (app) { // Passport app.use(passport.initialize()); app.use(passport.session()); + + // Configure GAE proxy and health checks. Force HTTPS. + app.enable('trust proxy'); + app.use(yesHttps({ maxAge: 31536000, includeSubdomains: true, preload: true })); + app.get('/_ah/health', (req, res) => { + res.sendStatus(200); + }); }; diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..a23b2df --- /dev/null +++ b/openapi.json @@ -0,0 +1,131 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "GDG-X Hub API", + "description": "API for using data from the Google Developer Groups (GDG) Hub.", + "license": { + "name": "MIT" + }, + "contact": { + "name": "GDG-X Support", + "email": "support@gdgx.io" + } + }, + "host": "api.gdgx.io", + "basePath": "/api/v1", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "paths": { + "/chapters": { + "get": { + "operationId": "listChapters", + "description": "Returns a list containing all Chapters", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/ChapterList" + } + } + } + } + } + }, + "definitions": { + "ChapterList": { + "type": "object", + "properties": { + "count": { + "type": "number" + }, + "pages": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perpage": { + "type": "number" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Chapter" + } + } + } + }, + "Chapter": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "site": { + "type": "string" + }, + "group_type": { + "type": "string" + }, + "country": { + "type": "string" + }, + "state": { + "type": "string" + }, + "city": { + "type": "string" + }, + "name": { + "type": "string" + }, + "__v": { + "type": "number" + }, + "organizers": { + "type": "array", + "items": { + "type": "string" + } + }, + "geo": { + "$ref": "#/definitions/Location" + } + } + }, + "Location": { + "type": "object", + "required": [ + "lng", "lat" + ], + "properties": { + "lng": { + "type": "number" + }, + "lat": { + "type": "number" + } + } + } + }, + "security": [ + + ], + "securityDefinitions": { + + } +} diff --git a/package.json b/package.json index 75a749b..9d75d32 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "async": "1.4.2", "body-parser": "1.17.1", + "bower": "1.8.0", "burrito": "0.2.12", "cacher": "1.0.0", "cheerio": "0.19.0", @@ -34,32 +35,10 @@ "express": "4.15.2", "express-rate": "0.0.1", "express-session": "1.15.1", + "jpegtran-bin": "0.2.0", + "jshint-stylish": "2.2.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", - "lodash": "2.4.1", - "method-override": "2.3.7", - "methods": "1.1.2", - "moment": "2.10.6", - "mongoose": "3.5.5", - "morgan": "1.8.1", - "node-forge": "0.6.34", - "node-geocoder": "2.2.0", - "node-uuid": "1.4.3", - "nodemailer": "1.4.0", - "passport": "0.3.2", - "passport-http-bearer": "1.0.1", - "passport-http-oauth": "0.1.3", - "passport-localapikey": "0.0.3", - "serve-favicon": "2.4.1", - "slugify": "0.1.1", - "socket.io": "0.9.17", - "superagent": "0.17.0", - "superagent-retry": "0.3.0", - "timequeue": "0.2.2" - }, - "devDependencies": { - "bower": "1.7.9", - "connect-livereload": "0.6.0", "grunt": "0.4.5", "grunt-autoprefixer": "0.4.0", "grunt-bower-install": "0.7.0", @@ -85,10 +64,35 @@ "grunt-rev": "0.1.0", "grunt-svgmin": "0.2.0", "grunt-usemin": "2.0.0", - "jasmine-core": "2.4.1", - "jpegtran-bin": "0.2.0", - "jshint-stylish": "2.2.1", "karma": "1.2.0", + "load-grunt-tasks": "0.2.0", + "lodash": "2.4.1", + "method-override": "2.3.7", + "methods": "1.1.2", + "moment": "2.10.6", + "mongoose": "3.5.5", + "morgan": "1.8.1", + "node-forge": "0.6.34", + "node-geocoder": "2.2.0", + "node-uuid": "1.4.3", + "nodemailer": "1.4.0", + "passport": "0.3.2", + "passport-http-bearer": "1.0.1", + "passport-http-oauth": "0.1.3", + "passport-localapikey": "0.0.3", + "requirejs": "2.1.20", + "serve-favicon": "2.4.1", + "slugify": "0.1.1", + "socket.io": "0.9.17", + "superagent": "0.17.0", + "superagent-retry": "0.3.0", + "timequeue": "0.2.2", + "time-grunt": "0.2.10", + "yes-https": "0.0.3" + }, + "devDependencies": { + "connect-livereload": "0.6.0", + "jasmine-core": "2.4.1", "karma-chrome-launcher": "2.0.0", "karma-coverage": "1.1.1", "karma-firefox-launcher": "1.0.0", @@ -100,20 +104,17 @@ "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", - "load-grunt-tasks": "0.2.0", - "phantomjs-prebuilt": "2.1.7", - "requirejs": "2.1.20", - "time-grunt": "0.2.10" + "phantomjs-prebuilt": "2.1.7" }, "engines": { - "node": ">=0.12.0" + "node": ">=6" }, "scripts": { - "postinstall": "bower install", - "prestart": "grunt", - "start": "grunt serve", - "startProd": "grunt serve:dist", - "configProd": "sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000;export PORT=3000;", + "deploy": "gcloud app deploy --project gdgx-cloud --no-promote", + "deploy-promote": "gcloud app deploy --project gdgx-cloud", + "deploy-api": "gcloud service-management deploy openapi.json --project gdgx-cloud", + "start": "grunt serve:dist", + "startDev": "grunt serve", "test": "grunt test" } } From 66db763596369abaecc9e18352550785b983b9fe Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 00:13:08 -0400 Subject: [PATCH 06/25] update(server): enable cloud debug and cloud trace --- package.json | 8 +++++--- server.js | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 9d75d32..5040161 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ }, "version": "0.2.1", "dependencies": { + "@google-cloud/debug-agent": "1.0.0", + "@google-cloud/trace-agent": "1.0.1", "async": "1.4.2", "body-parser": "1.17.1", "bower": "1.8.0", @@ -35,8 +37,6 @@ "express": "4.15.2", "express-rate": "0.0.1", "express-session": "1.15.1", - "jpegtran-bin": "0.2.0", - "jshint-stylish": "2.2.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", "grunt": "0.4.5", @@ -64,6 +64,8 @@ "grunt-rev": "0.1.0", "grunt-svgmin": "0.2.0", "grunt-usemin": "2.0.0", + "jpegtran-bin": "0.2.0", + "jshint-stylish": "2.2.1", "karma": "1.2.0", "load-grunt-tasks": "0.2.0", "lodash": "2.4.1", @@ -86,8 +88,8 @@ "socket.io": "0.9.17", "superagent": "0.17.0", "superagent-retry": "0.3.0", - "timequeue": "0.2.2", "time-grunt": "0.2.10", + "timequeue": "0.2.2", "yes-https": "0.0.3" }, "devDependencies": { diff --git a/server.js b/server.js index e7101ac..daabf19 100755 --- a/server.js +++ b/server.js @@ -1,5 +1,7 @@ 'use strict'; +require('@google-cloud/trace-agent').start(); + var env = require('dotenv').config({path: process.env.NODE_ENV === 'production' ? '.env-prod' : '.env'}); if (env && !env.error) { if (process.env.NODE_ENV !== 'production') { @@ -13,6 +15,7 @@ var express = require('express'), path = require('path'), fs = require('fs'), mongoose = require('mongoose'); +require('@google-cloud/debug-agent').start({ allowExpressions: true }); /** * Main application file From c5c854fde60960cce38db486e9df1c6582c7d2ef Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 00:16:09 -0400 Subject: [PATCH 07/25] fix(build): jshint and karma warnings --- lib/config/express.js | 2 +- test/spec/developer.spec.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index 8906900..69be2d5 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -7,13 +7,13 @@ var express = require('express'), favicon = require('serve-favicon'), path = require('path'), passport = require('passport'), - csrf = require('csurf'), config = require('./config'); var errorhandler = require('errorhandler'); var morgan = require('morgan'); var compression = require('compression'); var bodyParser = require('body-parser'); const MongoStore = require('connect-mongo')(session); +// var csrf = require('csurf'); /** * Express configuration diff --git a/test/spec/developer.spec.js b/test/spec/developer.spec.js index 918dab1..3bfc7fd 100644 --- a/test/spec/developer.spec.js +++ b/test/spec/developer.spec.js @@ -15,19 +15,19 @@ describe('Controller: DeveloperCtrl', function() { $scope: scope }); - $httpBackend.expectGET('/api/v1/rest').respond({ - baseUrl: 'https://hub.gdgx.io/api/v1/', - description: 'Everything GDG', - kind: 'discovery#restDescription', - name: 'hub', - ownerDomain: 'hub.gdgx.io', - ownerName: 'GDG[x]' - }); + // $httpBackend.expectGET('/api/v1/rest').respond({ + // baseUrl: 'https://hub.gdgx.io/api/v1/', + // description: 'Everything GDG', + // kind: 'discovery#restDescription', + // name: 'hub', + // ownerDomain: 'hub.gdgx.io', + // ownerName: 'GDG[x]' + // }); })); it('should set the API Docs data on the scope', function() { expect(scope.restDiscovery).toBeUndefined(); - $httpBackend.flush(); - expect(scope.restDiscovery.name).toBe('hub'); + // $httpBackend.flush(); + // expect(scope.restDiscovery.name).toBe('hub'); }); }); From f44fa9b0f222e4330de056eadeba276773d51c87 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 00:19:06 -0400 Subject: [PATCH 08/25] update(ci): drop testing of Node 0.12 and 5.x. Add 7.x testing. --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c47808e..554d7c8 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - - '0.12' - '4' - - '5' - '6' + - '7' before_script: - - npm install -g bower grunt-cli - - bower install + - npm install -g grunt-cli services: mongodb From b27772cd4816d2376f0d96f6b2917875fcd31e66 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 02:30:20 -0400 Subject: [PATCH 09/25] update(express): clarify cache config. disable yes-https for now. --- lib/config/express.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index 45aaa61..5ad7388 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -8,14 +8,17 @@ var express = require('express'), path = require('path'), passport = require('passport'), config = require('./config'); -var errorhandler = require('errorhandler'); +var errorHandler = require('errorhandler'); var morgan = require('morgan'); var compression = require('compression'); var bodyParser = require('body-parser'); -var yesHttps = require('yes-https'); +// var yesHttps = require('yes-https'); const MongoStore = require('connect-mongo')(session); // var csrf = require('csurf'); +const STATIC_FILE_CACHE_AGE = 604800000; // 1 week +// const YES_HTTPS_MAX_AGE = 120000; // 2 minutes + /** * Express configuration */ @@ -35,7 +38,7 @@ module.exports = function (app) { app.use(express.static(path.join(config.root, '.tmp'))); app.use(express.static(path.join(config.root, 'app'))); - app.use(errorhandler()); + app.use(errorHandler()); app.set('views', config.root + '/app/views'); app.use(session({ @@ -50,7 +53,7 @@ module.exports = function (app) { if (process.env.NODE_ENV === 'production') { app.use(favicon(path.join(config.root, 'public', 'favicon.ico'), {})); app.use(compression()); - app.use(express.static(path.join(config.root, 'public'), {maxAge: 604800000})); + app.use(express.static(path.join(config.root, 'public'), { maxAge: STATIC_FILE_CACHE_AGE, etag: true })); app.set('views', config.root + '/views'); app.use(session({ @@ -77,7 +80,7 @@ module.exports = function (app) { // Configure GAE proxy and health checks. Force HTTPS. app.enable('trust proxy'); - app.use(yesHttps({ maxAge: 31536000, includeSubdomains: true, preload: true })); + // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); app.get('/_ah/health', (req, res) => { res.sendStatus(200); }); From ca34f2778eec674fc5503a011c229391c3df6bfe Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 03:20:56 -0400 Subject: [PATCH 10/25] update(ci): revert change to remove bower install --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 554d7c8..de1ef79 100755 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: - '6' - '7' before_script: - - npm install -g grunt-cli + - npm install -g bower grunt-cli + - bower install services: mongodb From 429e24b26b2c93bc1b499b0085b6c0397e16e899 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 03:25:53 -0400 Subject: [PATCH 11/25] update(npm): update uglify and phantomjs trying to get rid of `sys` is deprecated warning, but no luck --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5040161..805f854 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "grunt-contrib-htmlmin": "0.1.3", "grunt-contrib-imagemin": "1.0.1", "grunt-contrib-jshint": "1.0.0", - "grunt-contrib-uglify": "0.2.0", + "grunt-contrib-uglify": "2.2.0", "grunt-contrib-watch": "0.5.2", "grunt-express-server": "0.4.5", "grunt-google-cdn": "0.2.0", @@ -106,7 +106,7 @@ "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", - "phantomjs-prebuilt": "2.1.7" + "phantomjs-prebuilt": "2.1.14" }, "engines": { "node": ">=6" From 3db4882fa037c6e4a27bdd35e5a916bc6ab10cba Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 05:54:06 -0400 Subject: [PATCH 12/25] update(server): changes that finally got the API working on GAE Flex!111!!11 update mongoose use default network since Flex doesn't seem to work on Legacy network --- app.yaml | 2 +- lib/config/env/all.js | 9 +++++++-- lib/config/express.js | 2 +- package.json | 4 +--- server.js | 47 ++++++++++++++++++++++++++----------------- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app.yaml b/app.yaml index 80eef9d..3b91a68 100644 --- a/app.yaml +++ b/app.yaml @@ -8,7 +8,7 @@ endpoints_api_service: config_id: 2017-03-18r0 network: instance_tag: https-server - name: gdg-x + name: default automatic_scaling: min_num_instances: 1 max_num_instances: 2 diff --git a/lib/config/env/all.js b/lib/config/env/all.js index fc57f7d..77c65dd 100755 --- a/lib/config/env/all.js +++ b/lib/config/env/all.js @@ -6,12 +6,17 @@ var rootPath = path.normalize(__dirname + '/../../..'); module.exports = { root: rootPath, - port: process.env.PORT || process.env.NODEJS_PORT || 3000, - hostname: process.env.NODEJS_IP || undefined, + port: process.env.PORT || 3000, + hostname: undefined, mongo: { options: { db: { safe: true + }, + server: { + socketOptions: { + keepAlive: 1 + } } } }, diff --git a/lib/config/express.js b/lib/config/express.js index 5ad7388..c8443d6 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -54,7 +54,7 @@ module.exports = function (app) { app.use(favicon(path.join(config.root, 'public', 'favicon.ico'), {})); app.use(compression()); app.use(express.static(path.join(config.root, 'public'), { maxAge: STATIC_FILE_CACHE_AGE, etag: true })); - app.set('views', config.root + '/views'); + app.set('views', config.root + '/public/views'); app.use(session({ cookie: { secure: true }, diff --git a/package.json b/package.json index 805f854..1927d8e 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "grunt-cli": "1.2.0", "grunt-concurrent": "0.4.1", "grunt-contrib-clean": "0.5.0", - "grunt-contrib-coffee": "0.7.0", "grunt-contrib-compass": "0.6.0", "grunt-contrib-concat": "0.3.0", "grunt-contrib-copy": "0.4.1", @@ -72,7 +71,7 @@ "method-override": "2.3.7", "methods": "1.1.2", "moment": "2.10.6", - "mongoose": "3.5.5", + "mongoose": "4.9.0", "morgan": "1.8.1", "node-forge": "0.6.34", "node-geocoder": "2.2.0", @@ -102,7 +101,6 @@ "karma-jasmine": "1.0.2", "karma-junit-reporter": "1.1.0", "karma-ng-html2js-preprocessor": "1.0.0", - "karma-ng-scenario": "1.0.0", "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", diff --git a/server.js b/server.js index daabf19..1fea78f 100755 --- a/server.js +++ b/server.js @@ -1,6 +1,8 @@ 'use strict'; -require('@google-cloud/trace-agent').start(); +if (process.env.NODE_ENV === 'production') { + require('@google-cloud/trace-agent').start(); +} var env = require('dotenv').config({path: process.env.NODE_ENV === 'production' ? '.env-prod' : '.env'}); if (env && !env.error) { @@ -16,6 +18,7 @@ var express = require('express'), fs = require('fs'), mongoose = require('mongoose'); require('@google-cloud/debug-agent').start({ allowExpressions: true }); +var app = express(); /** * Main application file @@ -48,30 +51,38 @@ fs.readdirSync(modelsPath).forEach(function (file) { require(modelsPath + '/' + file); }); -// Import static data -require('./lib/fixtures')(); +db.connection + .on('error', console.error) + .on('disconnected', console.error) + .once('open', listen); -// Passport Configuration -require('./lib/config/passport')(); +function listen() { + console.log('Connected to MongoDb at ' + + `mongodb://${db.connection.host}/${db.connection.name}:${db.connection.port}.`); -// Initialize admin task handlers -require('./lib/tasks')(); + // Import static data + require('./lib/fixtures')(); -var app = express(); + // Passport Configuration + require('./lib/config/passport')(); -// Express settings -require('./lib/config/express')(app); + // Initialize admin task handlers + require('./lib/tasks')(); -// Routing -require('./lib/routes')(app); + // Express settings + require('./lib/config/express')(app); -// Initialize cron jobs -require('./lib/cron')(); + // Routing + require('./lib/routes')(app); -// Start server -app.listen(config.port, config.hostname, function () { - console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); -}); + // Initialize cron jobs + require('./lib/cron')(); + + // Start server + app.listen(config.port, config.hostname, function() { + console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); + }); +} // Expose app exports = module.exports = app; From 3d00dc1fdb14948a051b85fb091e6077d9f3d885 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 16:04:37 -0400 Subject: [PATCH 13/25] update(server): move health checks up so that they aren't double logged --- lib/config/express.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index c8443d6..ccfdc4d 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -65,6 +65,13 @@ module.exports = function (app) { })); } + // Configure GAE proxy and health checks. Force HTTPS. + app.enable('trust proxy'); + // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); + app.get('/_ah/health', (req, res) => { + res.sendStatus(200); + }); + app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(morgan('[' + process.pid + '] :method :url :status :response-time ms - :res[content-length]')); @@ -77,11 +84,4 @@ module.exports = function (app) { // Passport app.use(passport.initialize()); app.use(passport.session()); - - // Configure GAE proxy and health checks. Force HTTPS. - app.enable('trust proxy'); - // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); - app.get('/_ah/health', (req, res) => { - res.sendStatus(200); - }); }; From 17c1f3e3050437a2fcb5fe130baaa13dbef6ff90 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 18:41:05 -0400 Subject: [PATCH 14/25] update(server): remote rate metering it relied on `express-rate` which hasn't been updated in 5 years and depends on deprecated functions in NodeJS we will use Cloud Endpoints for rate metering instead Closes #115 --- lib/controllers/api/index.js | 43 ------------------------------------ package.json | 1 - 2 files changed, 44 deletions(-) diff --git a/lib/controllers/api/index.js b/lib/controllers/api/index.js index dd173f3..63d7654 100755 --- a/lib/controllers/api/index.js +++ b/lib/controllers/api/index.js @@ -1,7 +1,6 @@ 'use strict'; var express = require('express'), - rate = require('express-rate'), config = require('../../config/config'), mongoose = require('mongoose'), SimpleApiKey = mongoose.model('SimpleApiKey'), @@ -13,13 +12,9 @@ var express = require('express'), module.exports = function (app) { const EDGE_CACHE_MAX_AGE = 3600; // 1 hr var versions = []; - var rateHandler; var cacher; utils.fixCacher(Cacher); - - console.log('Using in-memory rate handler...'); - rateHandler = new rate.Memory.MemoryRateHandler(); // In-Memory Cache cacher = new Cacher(); @@ -66,43 +61,6 @@ module.exports = function (app) { } }; - var rateMiddleware = function (req, res, next) { - - var limit = 10000; - - if (req.apikey) { - limit = 50000; - } - - var rm = rate.middleware({ - handler: rateHandler, - limit: limit, - interval: 86400, - setHeadersHandler: function (req, res, rate, limit, resetTime) { - var remaining = limit - rate; - - if (remaining < 0) { - remaining = 0; - } - res.setHeader('X-RateLimit-Limit', limit); - res.setHeader('X-RateLimit-Remaining', remaining); - res.setHeader('X-RateLimit-Reset', resetTime); - }, - onLimitReached: function (req, res, rate, limit, resetTime, next) { // jshint ignore:line - res.json(403, {error: 'Rate limit exceeded. Check headers for limit information.'}); - }, - getRouteKey: function (req) { // jshint ignore:line - return 'api'; - }, - getRemoteKey: function (req) { - return req.headers['x-client-ip'] || req.ip; - } - }); - - rm(req, res, next); - }; - - /** * Enable Google Edge caching for our API * @param req @@ -123,7 +81,6 @@ module.exports = function (app) { var impl = express(); impl.use(apiKeyMiddleware); - impl.use(rateMiddleware); impl.use(analyticsMiddleware(version)); impl.use(edgeCache); diff --git a/package.json b/package.json index 75a749b..b6a5b69 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "ejs": "0.8.4", "errorhandler": "1.5.0", "express": "4.15.2", - "express-rate": "0.0.1", "express-session": "1.15.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", From 93930d8650463913acfdf5e03b260cff48814974 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Fri, 17 Mar 2017 20:43:10 -0400 Subject: [PATCH 15/25] update(server): setup for GAE Flex and Cloud Endpoints. Relates to #100 --- .dockerignore | 16 ++++++ .gitignore | 16 +++--- .nvmrc | 2 +- Gruntfile.js | 3 +- app.yaml | 15 +++++ cron.yaml | 4 ++ lib/config/express.js | 8 +++ openapi.json | 131 ++++++++++++++++++++++++++++++++++++++++++ package.json | 75 ++++++++++++------------ 9 files changed, 223 insertions(+), 47 deletions(-) create mode 100644 .dockerignore create mode 100644 app.yaml create mode 100644 cron.yaml create mode 100644 openapi.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0d1c2a7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +test/ + +node_modules/ +.git/ +coverage/ +npm-debug.log + +.dockerignore +Dockerfile + +.gitignore +.gitattributes +.editorconfig +.idea/ +*.md +*.iml diff --git a/.gitignore b/.gitignore index 2981ae5..268d94a 100755 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,17 @@ .DS_Store -node_modules -public -.tmp +/node_modules +/public +/.tmp +/tmp .sass-cache -app/bower_components -heroku +/app/bower_components /views -dist -.idea +/dist +/.idea newrelic_agent.log .c9 mongodb +*.log .env .env-prod +npm-debug.log.* diff --git a/.nvmrc b/.nvmrc index 8ac28bf..6b9255c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -4.6.1 +6.9.2 diff --git a/Gruntfile.js b/Gruntfile.js index c7cffc8..c3124c9 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -282,7 +282,6 @@ module.exports = function (grunt) { src: [ 'package.json', 'server.js', - 'newrelic.js', 'lib/**/*' ] }] @@ -352,7 +351,7 @@ module.exports = function (grunt) { grunt.registerTask('serve', function (target) { if (target === 'dist') { - return grunt.task.run(['build', 'express:prod', 'open', 'express-keepalive']); + return grunt.task.run(['build', 'express:prod', 'express-keepalive']); } grunt.task.run([ diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000..80eef9d --- /dev/null +++ b/app.yaml @@ -0,0 +1,15 @@ +runtime: nodejs +env: flex +handlers: +- url: /.* + script: IGNORED +endpoints_api_service: + name: api.gdgx.io + config_id: 2017-03-18r0 +network: + instance_tag: https-server + name: gdg-x +automatic_scaling: + min_num_instances: 1 + max_num_instances: 2 + cool_down_period_sec: 300 diff --git a/cron.yaml b/cron.yaml new file mode 100644 index 0000000..7276ce1 --- /dev/null +++ b/cron.yaml @@ -0,0 +1,4 @@ +cron: +- description: daily 9am EST ingestion job + url: / + schedule: every day 13:00 diff --git a/lib/config/express.js b/lib/config/express.js index 69be2d5..45aaa61 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -12,6 +12,7 @@ var errorhandler = require('errorhandler'); var morgan = require('morgan'); var compression = require('compression'); var bodyParser = require('body-parser'); +var yesHttps = require('yes-https'); const MongoStore = require('connect-mongo')(session); // var csrf = require('csurf'); @@ -73,4 +74,11 @@ module.exports = function (app) { // Passport app.use(passport.initialize()); app.use(passport.session()); + + // Configure GAE proxy and health checks. Force HTTPS. + app.enable('trust proxy'); + app.use(yesHttps({ maxAge: 31536000, includeSubdomains: true, preload: true })); + app.get('/_ah/health', (req, res) => { + res.sendStatus(200); + }); }; diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..a23b2df --- /dev/null +++ b/openapi.json @@ -0,0 +1,131 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "GDG-X Hub API", + "description": "API for using data from the Google Developer Groups (GDG) Hub.", + "license": { + "name": "MIT" + }, + "contact": { + "name": "GDG-X Support", + "email": "support@gdgx.io" + } + }, + "host": "api.gdgx.io", + "basePath": "/api/v1", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "https" + ], + "paths": { + "/chapters": { + "get": { + "operationId": "listChapters", + "description": "Returns a list containing all Chapters", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/ChapterList" + } + } + } + } + } + }, + "definitions": { + "ChapterList": { + "type": "object", + "properties": { + "count": { + "type": "number" + }, + "pages": { + "type": "number" + }, + "page": { + "type": "number" + }, + "perpage": { + "type": "number" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Chapter" + } + } + } + }, + "Chapter": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "site": { + "type": "string" + }, + "group_type": { + "type": "string" + }, + "country": { + "type": "string" + }, + "state": { + "type": "string" + }, + "city": { + "type": "string" + }, + "name": { + "type": "string" + }, + "__v": { + "type": "number" + }, + "organizers": { + "type": "array", + "items": { + "type": "string" + } + }, + "geo": { + "$ref": "#/definitions/Location" + } + } + }, + "Location": { + "type": "object", + "required": [ + "lng", "lat" + ], + "properties": { + "lng": { + "type": "number" + }, + "lat": { + "type": "number" + } + } + } + }, + "security": [ + + ], + "securityDefinitions": { + + } +} diff --git a/package.json b/package.json index b6a5b69..28dfb93 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "async": "1.4.2", "body-parser": "1.17.1", + "bower": "1.8.0", "burrito": "0.2.12", "cacher": "1.0.0", "cheerio": "0.19.0", @@ -33,32 +34,10 @@ "errorhandler": "1.5.0", "express": "4.15.2", "express-session": "1.15.1", + "jpegtran-bin": "0.2.0", + "jshint-stylish": "2.2.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", - "lodash": "2.4.1", - "method-override": "2.3.7", - "methods": "1.1.2", - "moment": "2.10.6", - "mongoose": "3.5.5", - "morgan": "1.8.1", - "node-forge": "0.6.34", - "node-geocoder": "2.2.0", - "node-uuid": "1.4.3", - "nodemailer": "1.4.0", - "passport": "0.3.2", - "passport-http-bearer": "1.0.1", - "passport-http-oauth": "0.1.3", - "passport-localapikey": "0.0.3", - "serve-favicon": "2.4.1", - "slugify": "0.1.1", - "socket.io": "0.9.17", - "superagent": "0.17.0", - "superagent-retry": "0.3.0", - "timequeue": "0.2.2" - }, - "devDependencies": { - "bower": "1.7.9", - "connect-livereload": "0.6.0", "grunt": "0.4.5", "grunt-autoprefixer": "0.4.0", "grunt-bower-install": "0.7.0", @@ -84,10 +63,35 @@ "grunt-rev": "0.1.0", "grunt-svgmin": "0.2.0", "grunt-usemin": "2.0.0", - "jasmine-core": "2.4.1", - "jpegtran-bin": "0.2.0", - "jshint-stylish": "2.2.1", "karma": "1.2.0", + "load-grunt-tasks": "0.2.0", + "lodash": "2.4.1", + "method-override": "2.3.7", + "methods": "1.1.2", + "moment": "2.10.6", + "mongoose": "3.5.5", + "morgan": "1.8.1", + "node-forge": "0.6.34", + "node-geocoder": "2.2.0", + "node-uuid": "1.4.3", + "nodemailer": "1.4.0", + "passport": "0.3.2", + "passport-http-bearer": "1.0.1", + "passport-http-oauth": "0.1.3", + "passport-localapikey": "0.0.3", + "requirejs": "2.1.20", + "serve-favicon": "2.4.1", + "slugify": "0.1.1", + "socket.io": "0.9.17", + "superagent": "0.17.0", + "superagent-retry": "0.3.0", + "timequeue": "0.2.2", + "time-grunt": "0.2.10", + "yes-https": "0.0.3" + }, + "devDependencies": { + "connect-livereload": "0.6.0", + "jasmine-core": "2.4.1", "karma-chrome-launcher": "2.0.0", "karma-coverage": "1.1.1", "karma-firefox-launcher": "1.0.0", @@ -99,20 +103,17 @@ "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", - "load-grunt-tasks": "0.2.0", - "phantomjs-prebuilt": "2.1.7", - "requirejs": "2.1.20", - "time-grunt": "0.2.10" + "phantomjs-prebuilt": "2.1.7" }, "engines": { - "node": ">=0.12.0" + "node": ">=6" }, "scripts": { - "postinstall": "bower install", - "prestart": "grunt", - "start": "grunt serve", - "startProd": "grunt serve:dist", - "configProd": "sudo iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000;export PORT=3000;", + "deploy": "gcloud app deploy --project gdgx-cloud --no-promote", + "deploy-promote": "gcloud app deploy --project gdgx-cloud", + "deploy-api": "gcloud service-management deploy openapi.json --project gdgx-cloud", + "start": "grunt serve:dist", + "startDev": "grunt serve", "test": "grunt test" } } From 1f3f57bed672f08fec33ee614c5a9221bad5b766 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 00:13:08 -0400 Subject: [PATCH 16/25] update(server): enable cloud debug and cloud trace --- package.json | 8 +++++--- server.js | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 28dfb93..56d751f 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ }, "version": "0.2.1", "dependencies": { + "@google-cloud/debug-agent": "1.0.0", + "@google-cloud/trace-agent": "1.0.1", "async": "1.4.2", "body-parser": "1.17.1", "bower": "1.8.0", @@ -34,8 +36,6 @@ "errorhandler": "1.5.0", "express": "4.15.2", "express-session": "1.15.1", - "jpegtran-bin": "0.2.0", - "jshint-stylish": "2.2.1", "google-oauth-jwt": "0.1.7", "googleapis": "0.8.0", "grunt": "0.4.5", @@ -63,6 +63,8 @@ "grunt-rev": "0.1.0", "grunt-svgmin": "0.2.0", "grunt-usemin": "2.0.0", + "jpegtran-bin": "0.2.0", + "jshint-stylish": "2.2.1", "karma": "1.2.0", "load-grunt-tasks": "0.2.0", "lodash": "2.4.1", @@ -85,8 +87,8 @@ "socket.io": "0.9.17", "superagent": "0.17.0", "superagent-retry": "0.3.0", - "timequeue": "0.2.2", "time-grunt": "0.2.10", + "timequeue": "0.2.2", "yes-https": "0.0.3" }, "devDependencies": { diff --git a/server.js b/server.js index e7101ac..daabf19 100755 --- a/server.js +++ b/server.js @@ -1,5 +1,7 @@ 'use strict'; +require('@google-cloud/trace-agent').start(); + var env = require('dotenv').config({path: process.env.NODE_ENV === 'production' ? '.env-prod' : '.env'}); if (env && !env.error) { if (process.env.NODE_ENV !== 'production') { @@ -13,6 +15,7 @@ var express = require('express'), path = require('path'), fs = require('fs'), mongoose = require('mongoose'); +require('@google-cloud/debug-agent').start({ allowExpressions: true }); /** * Main application file From 5308488a8edeebc01c689c4d81c0d1660e631d39 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 00:19:06 -0400 Subject: [PATCH 17/25] update(ci): drop testing of Node 0.12 and 5.x. Add 7.x testing. --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c47808e..554d7c8 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - - '0.12' - '4' - - '5' - '6' + - '7' before_script: - - npm install -g bower grunt-cli - - bower install + - npm install -g grunt-cli services: mongodb From 4e8231f14e4e4b2b61a6c2f6104763b67d905be8 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 02:30:20 -0400 Subject: [PATCH 18/25] update(express): clarify cache config. disable yes-https for now. --- lib/config/express.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index 45aaa61..5ad7388 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -8,14 +8,17 @@ var express = require('express'), path = require('path'), passport = require('passport'), config = require('./config'); -var errorhandler = require('errorhandler'); +var errorHandler = require('errorhandler'); var morgan = require('morgan'); var compression = require('compression'); var bodyParser = require('body-parser'); -var yesHttps = require('yes-https'); +// var yesHttps = require('yes-https'); const MongoStore = require('connect-mongo')(session); // var csrf = require('csurf'); +const STATIC_FILE_CACHE_AGE = 604800000; // 1 week +// const YES_HTTPS_MAX_AGE = 120000; // 2 minutes + /** * Express configuration */ @@ -35,7 +38,7 @@ module.exports = function (app) { app.use(express.static(path.join(config.root, '.tmp'))); app.use(express.static(path.join(config.root, 'app'))); - app.use(errorhandler()); + app.use(errorHandler()); app.set('views', config.root + '/app/views'); app.use(session({ @@ -50,7 +53,7 @@ module.exports = function (app) { if (process.env.NODE_ENV === 'production') { app.use(favicon(path.join(config.root, 'public', 'favicon.ico'), {})); app.use(compression()); - app.use(express.static(path.join(config.root, 'public'), {maxAge: 604800000})); + app.use(express.static(path.join(config.root, 'public'), { maxAge: STATIC_FILE_CACHE_AGE, etag: true })); app.set('views', config.root + '/views'); app.use(session({ @@ -77,7 +80,7 @@ module.exports = function (app) { // Configure GAE proxy and health checks. Force HTTPS. app.enable('trust proxy'); - app.use(yesHttps({ maxAge: 31536000, includeSubdomains: true, preload: true })); + // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); app.get('/_ah/health', (req, res) => { res.sendStatus(200); }); From cce809a0d1454e846b3e79468c1aa3aa1898f5a5 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 03:20:56 -0400 Subject: [PATCH 19/25] update(ci): revert change to remove bower install --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 554d7c8..de1ef79 100755 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,6 @@ node_js: - '6' - '7' before_script: - - npm install -g grunt-cli + - npm install -g bower grunt-cli + - bower install services: mongodb From df8d2cd505b69289166d21576ea35dcd03f78729 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 03:25:53 -0400 Subject: [PATCH 20/25] update(npm): update uglify and phantomjs trying to get rid of `sys` is deprecated warning, but no luck --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 56d751f..8adc67b 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "grunt-contrib-htmlmin": "0.1.3", "grunt-contrib-imagemin": "1.0.1", "grunt-contrib-jshint": "1.0.0", - "grunt-contrib-uglify": "0.2.0", + "grunt-contrib-uglify": "2.2.0", "grunt-contrib-watch": "0.5.2", "grunt-express-server": "0.4.5", "grunt-google-cdn": "0.2.0", @@ -105,7 +105,7 @@ "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", - "phantomjs-prebuilt": "2.1.7" + "phantomjs-prebuilt": "2.1.14" }, "engines": { "node": ">=6" From f4beee35258930f6e0b4c84b7349a9861ee3b786 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 05:54:06 -0400 Subject: [PATCH 21/25] update(server): changes that finally got the API working on GAE Flex!111!!11 update mongoose use default network since Flex doesn't seem to work on Legacy network --- app.yaml | 2 +- lib/config/env/all.js | 9 +++++++-- lib/config/express.js | 2 +- package.json | 4 +--- server.js | 47 ++++++++++++++++++++++++++----------------- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/app.yaml b/app.yaml index 80eef9d..3b91a68 100644 --- a/app.yaml +++ b/app.yaml @@ -8,7 +8,7 @@ endpoints_api_service: config_id: 2017-03-18r0 network: instance_tag: https-server - name: gdg-x + name: default automatic_scaling: min_num_instances: 1 max_num_instances: 2 diff --git a/lib/config/env/all.js b/lib/config/env/all.js index fc57f7d..77c65dd 100755 --- a/lib/config/env/all.js +++ b/lib/config/env/all.js @@ -6,12 +6,17 @@ var rootPath = path.normalize(__dirname + '/../../..'); module.exports = { root: rootPath, - port: process.env.PORT || process.env.NODEJS_PORT || 3000, - hostname: process.env.NODEJS_IP || undefined, + port: process.env.PORT || 3000, + hostname: undefined, mongo: { options: { db: { safe: true + }, + server: { + socketOptions: { + keepAlive: 1 + } } } }, diff --git a/lib/config/express.js b/lib/config/express.js index 5ad7388..c8443d6 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -54,7 +54,7 @@ module.exports = function (app) { app.use(favicon(path.join(config.root, 'public', 'favicon.ico'), {})); app.use(compression()); app.use(express.static(path.join(config.root, 'public'), { maxAge: STATIC_FILE_CACHE_AGE, etag: true })); - app.set('views', config.root + '/views'); + app.set('views', config.root + '/public/views'); app.use(session({ cookie: { secure: true }, diff --git a/package.json b/package.json index 8adc67b..aba90bb 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "grunt-cli": "1.2.0", "grunt-concurrent": "0.4.1", "grunt-contrib-clean": "0.5.0", - "grunt-contrib-coffee": "0.7.0", "grunt-contrib-compass": "0.6.0", "grunt-contrib-concat": "0.3.0", "grunt-contrib-copy": "0.4.1", @@ -71,7 +70,7 @@ "method-override": "2.3.7", "methods": "1.1.2", "moment": "2.10.6", - "mongoose": "3.5.5", + "mongoose": "4.9.0", "morgan": "1.8.1", "node-forge": "0.6.34", "node-geocoder": "2.2.0", @@ -101,7 +100,6 @@ "karma-jasmine": "1.0.2", "karma-junit-reporter": "1.1.0", "karma-ng-html2js-preprocessor": "1.0.0", - "karma-ng-scenario": "1.0.0", "karma-phantomjs-launcher": "1.0.1", "karma-requirejs": "1.0.0", "karma-script-launcher": "1.0.0", diff --git a/server.js b/server.js index daabf19..1fea78f 100755 --- a/server.js +++ b/server.js @@ -1,6 +1,8 @@ 'use strict'; -require('@google-cloud/trace-agent').start(); +if (process.env.NODE_ENV === 'production') { + require('@google-cloud/trace-agent').start(); +} var env = require('dotenv').config({path: process.env.NODE_ENV === 'production' ? '.env-prod' : '.env'}); if (env && !env.error) { @@ -16,6 +18,7 @@ var express = require('express'), fs = require('fs'), mongoose = require('mongoose'); require('@google-cloud/debug-agent').start({ allowExpressions: true }); +var app = express(); /** * Main application file @@ -48,30 +51,38 @@ fs.readdirSync(modelsPath).forEach(function (file) { require(modelsPath + '/' + file); }); -// Import static data -require('./lib/fixtures')(); +db.connection + .on('error', console.error) + .on('disconnected', console.error) + .once('open', listen); -// Passport Configuration -require('./lib/config/passport')(); +function listen() { + console.log('Connected to MongoDb at ' + + `mongodb://${db.connection.host}/${db.connection.name}:${db.connection.port}.`); -// Initialize admin task handlers -require('./lib/tasks')(); + // Import static data + require('./lib/fixtures')(); -var app = express(); + // Passport Configuration + require('./lib/config/passport')(); -// Express settings -require('./lib/config/express')(app); + // Initialize admin task handlers + require('./lib/tasks')(); -// Routing -require('./lib/routes')(app); + // Express settings + require('./lib/config/express')(app); -// Initialize cron jobs -require('./lib/cron')(); + // Routing + require('./lib/routes')(app); -// Start server -app.listen(config.port, config.hostname, function () { - console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); -}); + // Initialize cron jobs + require('./lib/cron')(); + + // Start server + app.listen(config.port, config.hostname, function() { + console.log('Express server listening on port %d in %s mode', config.port, app.get('env')); + }); +} // Expose app exports = module.exports = app; From dc344c7d2d6bb43a96618bc13facbe9526c3690b Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 16:04:37 -0400 Subject: [PATCH 22/25] update(server): move health checks up so that they aren't double logged --- lib/config/express.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/config/express.js b/lib/config/express.js index c8443d6..ccfdc4d 100755 --- a/lib/config/express.js +++ b/lib/config/express.js @@ -65,6 +65,13 @@ module.exports = function (app) { })); } + // Configure GAE proxy and health checks. Force HTTPS. + app.enable('trust proxy'); + // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); + app.get('/_ah/health', (req, res) => { + res.sendStatus(200); + }); + app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(morgan('[' + process.pid + '] :method :url :status :response-time ms - :res[content-length]')); @@ -77,11 +84,4 @@ module.exports = function (app) { // Passport app.use(passport.initialize()); app.use(passport.session()); - - // Configure GAE proxy and health checks. Force HTTPS. - app.enable('trust proxy'); - // app.use(yesHttps({ maxAge: YES_HTTPS_MAX_AGE, includeSubdomains: true, preload: true })); - app.get('/_ah/health', (req, res) => { - res.sendStatus(200); - }); }; From a080d7c42652beb77875f8658ada556369d253d3 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Sat, 18 Mar 2017 19:07:31 -0400 Subject: [PATCH 23/25] fix(api): fix merge issue --- lib/controllers/api/index.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/controllers/api/index.js b/lib/controllers/api/index.js index c5289ae..63d7654 100755 --- a/lib/controllers/api/index.js +++ b/lib/controllers/api/index.js @@ -61,19 +61,6 @@ module.exports = function (app) { } }; - /** - * Enable Google Edge caching for our API - * @param req - * @param res - * @param next - */ - var edgeCache = function (req, res, next) { - res.header('Cache-Control', 'public, max-age=' + EDGE_CACHE_MAX_AGE); - res.header('Pragma', 'Public'); - next(); - }; - - /** * Enable Google Edge caching for our API * @param req From 8ab1945ca363fce942ab135472994d7e6dcc9293 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Tue, 21 Mar 2017 14:06:52 -0400 Subject: [PATCH 24/25] update(npm): update to latest csurf --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aba90bb..28376d6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "connect-mongo": "1.3.2", "cookie-parser": "1.4.3", "cron": "1.0.9", - "csurf": "1.8.3", + "csurf": "1.9.0", "dotenv": "4.0.0", "ejs": "0.8.4", "errorhandler": "1.5.0", From b2b22e5727f881577f50681c7114cf56714430a9 Mon Sep 17 00:00:00 2001 From: Michael Prentice Date: Wed, 10 May 2017 13:07:27 -0400 Subject: [PATCH 25/25] fix(keys): fix error when process.env.ANDROID_CLIENT_IDS is undefined Fixes #120 --- lib/config/keys.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config/keys.js b/lib/config/keys.js index 94b3e20..a644278 100755 --- a/lib/config/keys.js +++ b/lib/config/keys.js @@ -12,7 +12,7 @@ module.exports = { }, frisbee: { serverClientId: process.env.SERVER_KEY_SECRET, - androidClientIds: process.env.ANDROID_CLIENT_IDS.split(',') + androidClientIds: process.env.ANDROID_CLIENT_IDS ? process.env.ANDROID_CLIENT_IDS.split(',') : [] } } };