├── Procfile ├── .gitattributes ├── .gitignore ├── static ├── hero.png ├── logo.png ├── octocat.png ├── mixpanel.js ├── style.css └── newrelic.js ├── README.md ├── .env.format ├── views ├── head.pug ├── generate.pug ├── new.pug ├── done.pug ├── mixpanel.pug └── hi.pug ├── newrelic.js ├── package.json └── index.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | views linguist-documentation 2 | * linguist-language=JavaScript 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | node_modules 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /static/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthkp/pay-with-a-star/HEAD/static/hero.png -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthkp/pay-with-a-star/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siddharthkp/pay-with-a-star/HEAD/static/octocat.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pay with a tweet](https://github.com/siddharthkp/pay-with-a-star/raw/master/static/hero.png)](https://paywithastar.herokuapp.com) 2 | -------------------------------------------------------------------------------- /.env.format: -------------------------------------------------------------------------------- 1 | DEBUG=true # points to localhost in debug mode 2 | PORT=8080 3 | CLIENT_ID= github app client id 4 | CLIENT_SECRET=github app secret 5 | DATABASE_URL=path to database 6 | #MIXPANEL_TOKEN=mixpanel token (optional) 7 | -------------------------------------------------------------------------------- /views/head.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | include ./mixpanel.pug 7 | -------------------------------------------------------------------------------- /newrelic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * New Relic agent configuration. 5 | * 6 | * See lib/config.defaults.js in the agent distribution for a more complete 7 | * description of configuration variables and their potential values. 8 | */ 9 | exports.config = { 10 | /** 11 | * Array of application names. 12 | */ 13 | app_name: ['Pay with a star'], 14 | /** 15 | * Your New Relic license key. 16 | */ 17 | license_key: process.env.NEWRELIC_KEY, 18 | logging: { 19 | /** 20 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing 21 | * issues with the agent, 'info' and higher will impose the least overhead on 22 | * production applications. 23 | */ 24 | level: 'info' 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /views/generate.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pay with a star - Share link 4 | include ./head.pug 5 | 6 | 7 |
8 | 9 | 10 |
Here's your link
11 |
12 |
13 | 14 |
15 | script(type='text/javascript'). 16 | document.getElementById('share-link').addEventListener('click', function() { 17 | mixpanel.track('Click sharing link'); 18 | }); 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pay-with-a-star", 3 | "version": "1.0.0", 4 | "description": "Pay with a star", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "github.com/siddharthkp/pay-with-a-star" 13 | }, 14 | "keywords": [ 15 | "pay", 16 | "github", 17 | "star" 18 | ], 19 | "author": "siddharthkp", 20 | "license": "ISC", 21 | "dependencies": { 22 | "body-parser": "1.15.2", 23 | "dotenv": "2.0.0", 24 | "express": "4.14.0", 25 | "newrelic": "1.28.3", 26 | "pg": "6.0.3", 27 | "pug": "2.0.0-beta4", 28 | "raven": "0.11.0", 29 | "request": "2.74.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /views/new.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | Pay with a star - Generate a link 4 | include ./head.pug 5 | 6 | 7 |
8 | 9 | 10 |
Create your own link
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 | script(type='text/javascript'). 21 | mixpanel.track_forms('#new-form', 'Create link from new'); 22 | 23 | 24 | -------------------------------------------------------------------------------- /views/done.pug: -------------------------------------------------------------------------------- 1 | 2 | 3 | pay with a star - Thanks! 4 | include ./head.pug 5 | 6 | 7 |
8 | 9 | 10 |
Thanks for the star!
11 |
Taking you to
12 |
13 | script(type='text/javascript'). 14 | var redirect_url = location.search.split('=')[1]; 15 | document.getElementById('url').textContent = redirect_url; 16 | document.getElementById('url').href = redirect_url; 17 | mixpanel.track('Done page', {redirect_url: redirect_url}); 18 | if (redirect_url) { 19 | window.setTimeout(function() { 20 | window.location = redirect_url; 21 | }, 3000); 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /static/mixpanel.js: -------------------------------------------------------------------------------- 1 | (function(e,a){if(!a.__SV){var b=window;try{var c,l,i,j=b.location,g=j.hash;c=function(a,b){return(l=a.match(RegExp(b+"=([^&]*)")))?l[1]:null};g&&c(g,"state")&&(i=JSON.parse(decodeURIComponent(c(g,"state"))),"mpeditor"===i.action&&(b.sessionStorage.setItem("_mpcehash",g),history.replaceState(i.desiredHash||"",e.title,j.pathname+j.search)))}catch(m){}var k,h;window.mixpanel=a;a._i=[];a.init=function(b,c,f){function e(b,a){var c=a.split(".");2==c.length&&(b=b[c[0]],a=c[1]);b[a]=function(){b.push([a].concat(Array.prototype.slice.call(arguments, 2 | 0)))}}var d=a;"undefined"!==typeof f?d=a[f]=[]:f="mixpanel";d.people=d.people||[];d.toString=function(b){var a="mixpanel";"mixpanel"!==f&&(a+="."+f);b||(a+=" (stub)");return a};d.people.toString=function(){return d.toString(1)+".people (stub)"};k="disable time_event track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config reset people.set people.set_once people.increment people.append people.union people.track_charge people.clear_charges people.delete_user".split(" "); 3 | for(h=0;h 2 | 3 | Pay with a star 4 | include ./head.pug 5 | 6 | 7 |
8 | 9 | 10 |
get paid in stars
11 |
for your open source work
12 |
13 |
14 |
15 | 1 16 |
Generate your link
17 |
18 |
19 | 2 20 |
People star your repo
21 |
22 |
23 | 3 24 |
They get access to content
25 |
26 |
27 |
28 |
29 | try the demo link 30 |
31 |
32 |
or
33 |
34 | 35 | 36 | 37 | 38 |
39 |
40 | script(type='text/javascript'). 41 | mixpanel.track_links('#demo-link', 'Demo link from home'); 42 | mixpanel.track_forms('#home-form', 'Create link from home'); 43 | 44 | 45 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Assistant', sans-serif; 3 | height: 100%; 4 | font-weight: 200; 5 | font-size: 16px; 6 | background: #002635; 7 | color: FFF; 8 | max-width: 800px; 9 | margin: 0 auto; 10 | } 11 | 12 | /* Inputs and buttons */ 13 | 14 | .button { 15 | font-weight: bold; 16 | padding: 10px 20px; 17 | border-radius: 2px; 18 | color: #002635; 19 | display: inline-block; 20 | background: #F9CD3C; 21 | cursor: pointer; 22 | text-decoration: none; 23 | } 24 | .button:hover, .button:focus { 25 | background: #ffdc68; 26 | } 27 | 28 | input { 29 | border: none; 30 | font-family: 'Assistant', sans-serif; 31 | font-size: 1em; 32 | padding: 10px 20px; 33 | width: 60%; 34 | margin: 2px 0; 35 | border-radius: 2px; 36 | } 37 | input:focus { 38 | outline: none; 39 | } 40 | 41 | @media screen and (max-width: 600px) { 42 | input { 43 | width: 100%; 44 | } 45 | } 46 | 47 | /* Hero */ 48 | 49 | .hero { 50 | text-align: center; 51 | padding: 15% 5% 0; 52 | } 53 | .logo { 54 | height: 5em; 55 | margin-bottom: 3em; 56 | } 57 | .logo.octocat { 58 | margin-right: -50px; 59 | position: relative; 60 | } 61 | .tagline { 62 | font-size: 2.5em; 63 | color: #F9CD3C; 64 | } 65 | .subline { 66 | color: #EEE; 67 | font-size: 1.4em; 68 | } 69 | @media screen and (min-width: 600px) { 70 | .home input { 71 | width: 24%; 72 | } 73 | } 74 | 75 | /* How it works */ 76 | 77 | .how { 78 | margin: 12% auto; 79 | } 80 | .step { 81 | width: 33%; 82 | float: left; 83 | text-align: center; 84 | } 85 | .step .number { 86 | color: #F9CD3C; 87 | } 88 | .clear { 89 | clear: both; 90 | } 91 | 92 | @media screen and (max-width: 600px) { 93 | .how { 94 | width: 75%; 95 | } 96 | .step { 97 | width: 100%; 98 | margin: 1em 0; 99 | text-align: left; 100 | } 101 | .step .number { 102 | float: left; 103 | margin-right: 2em; 104 | } 105 | } 106 | 107 | /* Example */ 108 | 109 | .example { 110 | text-align: center; 111 | margin: 10% 0 10%; 112 | } 113 | .example .button { 114 | font-size: 1.2em; 115 | padding: 10px 30px; 116 | } 117 | 118 | @media screen and (max-width: 600px) { 119 | .example .button { 120 | font-size: 1em; 121 | } 122 | } 123 | 124 | 125 | /* Actions */ 126 | 127 | .action { 128 | text-align: center; 129 | width: 90%; 130 | margin: 0 auto; 131 | } 132 | .or { 133 | margin-bottom: 2em; 134 | } 135 | 136 | /* New page */ 137 | 138 | .new .hero { 139 | padding: 20% 5% 10%; 140 | } 141 | 142 | /* Done page */ 143 | 144 | #url { 145 | color: #ffeeb4; 146 | text-decoration: none; 147 | } 148 | .done .subline { 149 | margin-top: 1em; 150 | } 151 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | 5 | const debug = process.env.DEBUG; 6 | if (!debug && process.env.NEWRELIC_KEY) require('newrelic'); 7 | 8 | const express = require('express'); 9 | const request = require('request'); 10 | const pug = require('pug'); 11 | const bodyparser = require('body-parser'); 12 | const pg = require('pg'); 13 | 14 | const port = process.env.PORT; 15 | const client_id = process.env.CLIENT_ID; 16 | const client_secret = process.env.CLIENT_SECRET; 17 | const sentry_dsn = process.env.SENTRY_DSN; 18 | 19 | let host = 'https://paywithastar.herokuapp.com'; 20 | if (debug) host = 'http://localhost:' + port; 21 | 22 | const raven = require('raven'); 23 | const logger = new raven.Client(sentry_dsn); 24 | 25 | const app = express(); 26 | app.set('view engine', 'pug'); 27 | app.use(express.static('static')); 28 | app.use(bodyparser.urlencoded()); 29 | 30 | const mixpanel_token = process.env.MIXPANEL_TOKEN; 31 | app.locals.mixpanel_token = mixpanel_token; 32 | 33 | /* Server started */ 34 | app.listen(port, () => { 35 | logger.captureMessage('Server started', {level: 'info'}); 36 | }); 37 | 38 | let database; 39 | pg.defaults.ssl = true; 40 | pg.connect(process.env.DATABASE_URL, (err, client) => { 41 | if (err) throw err; 42 | database = client; 43 | }); 44 | 45 | /* Using code to get access token 46 | * 47 | * code = access code 48 | * params = {author, repo} 49 | * callback = callback function (starRepo) 50 | * res = express response object 51 | * 52 | */ 53 | 54 | let getAccessToken = (code, hash, callback, res) => { 55 | let options = { 56 | url:'https://github.com/login/oauth/access_token', 57 | headers: { 58 | 'Accept': 'application/json' 59 | }, 60 | form: { 61 | client_id, 62 | client_secret, 63 | code, 64 | scope: 'public_repo' 65 | } 66 | }; 67 | request.post(options, (err, httpResponse, body) => { 68 | let access_token = JSON.parse(body).access_token; 69 | logger.captureMessage('Oath', {level: 'debug', extra: JSON.parse(body)}); 70 | callback(access_token, hash, res); 71 | }); 72 | }; 73 | 74 | let getDataFromHash = (hash, callback) => { 75 | let query = "SELECT * from links where hash = '" + hash + "'"; 76 | logger.captureMessage('Query', {level: 'info', extra: query}); 77 | database.query(query, (err, result) => { 78 | if (err) throw err; 79 | let redirect_url; 80 | let repo; 81 | if (result.rows.length) { 82 | repo = result.rows[0].repo; 83 | redirect_url = result.rows[0].redirection; 84 | } else { 85 | redirect_url = host; 86 | } 87 | callback({repo, redirect_url}); 88 | }); 89 | }; 90 | 91 | /* Star repo 92 | * 93 | * token = github access token 94 | * hash = identifier 95 | * res = express response object 96 | * 97 | */ 98 | 99 | let starRepo = (token, hash, res) => { 100 | getDataFromHash(hash, (data) => { 101 | let url = 'https://api.github.com/user/starred/'; 102 | url += data.repo; 103 | url += '?access_token=' + token; 104 | 105 | let options = { 106 | url: url, 107 | headers: { 108 | 'Accept': 'application/json', 109 | 'User-Agent': 'pay-with-a-star' 110 | } 111 | }; 112 | 113 | request.put(options, (err, response, body) => { 114 | res.redirect('/done?redirect_url=' + data.redirect_url); 115 | }); 116 | }); 117 | }; 118 | 119 | let authorizeEndPoint = 'https://github.com/login/oauth/authorize?'; 120 | authorizeEndPoint += 'client_id=' + client_id; 121 | authorizeEndPoint += '&scope=public_repo'; 122 | authorizeEndPoint += '&redirect_uri='; 123 | authorizeEndPoint += host + '/r/'; 124 | 125 | /* s - entry point */ 126 | 127 | app.get('/s/:hash', (req, res) => { 128 | let hash = req.params.hash; 129 | res.redirect(authorizeEndPoint + hash) // redirect to authorize end point 130 | }); 131 | 132 | /* r - return url */ 133 | 134 | app.get('/r/:hash', (req, res) => { 135 | let code = req.query.code; 136 | let hash = req.params.hash; 137 | getAccessToken(code, hash, starRepo, res); 138 | }); 139 | 140 | /* Page before redirecting */ 141 | 142 | app.get('/done', (req, res) => { 143 | res.render('done'); 144 | }); 145 | 146 | /* New link page */ 147 | 148 | app.get('/new', (req, res) => { 149 | res.render('new'); 150 | }); 151 | 152 | /* Generate links */ 153 | 154 | let generateLink = (data, callback) => { 155 | let length = 6; 156 | let hash = (Math.random().toString(36)+'00000000000000000').slice(2, length +2); 157 | let link = host + '/s/' + hash; 158 | 159 | let repo = data.author + '/' + data.repo; 160 | 161 | let values = "'" + hash + "','" + repo + "','" + data.redirection + "', now()"; 162 | 163 | //let query = 'CREATE TABLE links (hash varchar(20), repo varchar(200), redirection varchar(200), created_on date)'; 164 | 165 | let query = "INSERT INTO links (hash, repo, redirection, created_on) values (" + values + ")"; 166 | logger.captureMessage('Query', {level: 'info', extra: query}); 167 | 168 | database.query(query, (err, result) => { 169 | if (err) throw err; 170 | callback(link); 171 | }); 172 | }; 173 | 174 | app.post('/generate', (req, res) => { 175 | let data = req.body; 176 | generateLink(data, (link) => { 177 | res.render('generate', {link}); 178 | }); 179 | }); 180 | 181 | /* Home page */ 182 | 183 | app.get('/', (req, res) => { 184 | res.render('hi'); 185 | }); 186 | 187 | /* Status end point */ 188 | 189 | app.get('/status', (req, res) => { 190 | res.end('Alive'); 191 | }); 192 | 193 | -------------------------------------------------------------------------------- /static/newrelic.js: -------------------------------------------------------------------------------- 1 | window.NREUM||(NREUM={}),__nr_require=function(t,e,n){function r(n){if(!e[n]){var o=e[n]={exports:{}};t[n][0].call(o.exports,function(e){var o=t[n][1][e];return r(o||e)},o,o.exports)}return e[n].exports}if("function"==typeof __nr_require)return __nr_require;for(var o=0;o0&&(d-=1)}),s.on("internal-error",function(t){i("ierr",[t,(new Date).getTime(),!0])})},{}],3:[function(t,e,n){t("loader").features.ins=!0},{}],4:[function(t,e,n){function r(t){}if(window.performance&&window.performance.timing&&window.performance.getEntriesByType){var o=t("ee"),i=t("handle"),a=t(8),s=t(7),c="learResourceTimings",f="addEventListener",u="resourcetimingbufferfull",d="bstResource",l="resource",p="-start",h="-end",m="fn"+p,w="fn"+h,v="bstTimer",y="pushState";t("loader").features.stn=!0,t(6);var g=NREUM.o.EV;o.on(m,function(t,e){var n=t[0];n instanceof g&&(this.bstStart=Date.now())}),o.on(w,function(t,e){var n=t[0];n instanceof g&&i("bst",[n,e,this.bstStart,Date.now()])}),a.on(m,function(t,e,n){this.bstStart=Date.now(),this.bstType=n}),a.on(w,function(t,e){i(v,[e,this.bstStart,Date.now(),this.bstType])}),s.on(m,function(){this.bstStart=Date.now()}),s.on(w,function(t,e){i(v,[e,this.bstStart,Date.now(),"requestAnimationFrame"])}),o.on(y+p,function(t){this.time=Date.now(),this.startPath=location.pathname+location.hash}),o.on(y+h,function(t){i("bstHist",[location.pathname+location.hash,this.startPath,this.time])}),f in window.performance&&(window.performance["c"+c]?window.performance[f](u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance["c"+c]()},!1):window.performance[f]("webkit"+u,function(t){i(d,[window.performance.getEntriesByType(l)]),window.performance["webkitC"+c]()},!1)),document[f]("scroll",r,!1),document[f]("keypress",r,!1),document[f]("click",r,!1)}},{}],5:[function(t,e,n){function r(t){for(var e=t;e&&!e.hasOwnProperty(u);)e=Object.getPrototypeOf(e);e&&o(e)}function o(t){s.inPlace(t,[u,d],"-",i)}function i(t,e){return t[1]}var a=t("ee").get("events"),s=t(17)(a),c=t("gos"),f=XMLHttpRequest,u="addEventListener",d="removeEventListener";e.exports=a,"getPrototypeOf"in Object?(r(document),r(window),r(f.prototype)):f.prototype.hasOwnProperty(u)&&(o(window),o(f.prototype)),a.on(u+"-start",function(t,e){if(t[1]){var n=t[1];if("function"==typeof n){var r=c(n,"nr@wrapped",function(){return s(n,"fn-",null,n.name||"anonymous")});this.wrapped=t[1]=r}else"function"==typeof n.handleEvent&&s.inPlace(n,["handleEvent"],"fn-")}}),a.on(d+"-start",function(t){var e=this.wrapped;e&&(t[1]=e)})},{}],6:[function(t,e,n){var r=t("ee").get("history"),o=t(17)(r);e.exports=r,o.inPlace(window.history,["pushState","replaceState"],"-")},{}],7:[function(t,e,n){var r=t("ee").get("raf"),o=t(17)(r),i="equestAnimationFrame";e.exports=r,o.inPlace(window,["r"+i,"mozR"+i,"webkitR"+i,"msR"+i],"raf-"),r.on("raf-start",function(t){t[0]=o(t[0],"fn-")})},{}],8:[function(t,e,n){function r(t,e,n){t[0]=a(t[0],"fn-",null,n)}function o(t,e,n){this.method=n,this.timerDuration="number"==typeof t[1]?t[1]:0,t[0]=a(t[0],"fn-",this,n)}var i=t("ee").get("timer"),a=t(17)(i),s="setTimeout",c="setInterval",f="clearTimeout",u="-start",d="-";e.exports=i,a.inPlace(window,[s,"setImmediate"],s+d),a.inPlace(window,[c],c+d),a.inPlace(window,[f,"clearImmediate"],f+d),i.on(c+u,r),i.on(s+u,o)},{}],9:[function(t,e,n){function r(t,e){d.inPlace(e,["onreadystatechange"],"fn-",s)}function o(){var t=this,e=u.context(t);t.readyState>3&&!e.resolved&&(e.resolved=!0,u.emit("xhr-resolved",[],t)),d.inPlace(t,w,"fn-",s)}function i(t){v.push(t),h&&(g=-g,b.data=g)}function a(){for(var t=0;t34||p<10)||window.opera||t.addEventListener("progress",function(t){e.lastSize=t.loaded},!1)}),f.on("open-xhr-start",function(t){this.params={method:t[0]},i(this,t[1]),this.metrics={}}),f.on("open-xhr-end",function(t,e){"loader_config"in NREUM&&"xpid"in NREUM.loader_config&&this.sameOrigin&&e.setRequestHeader("X-NewRelic-ID",NREUM.loader_config.xpid)}),f.on("send-xhr-start",function(t,e){var n=this.metrics,r=t[0],o=this;if(n&&r){var i=h(r);i&&(n.txSize=i)}this.startTime=(new Date).getTime(),this.listener=function(t){try{"abort"===t.type&&(o.params.aborted=!0),("load"!==t.type||o.called===o.totalCbs&&(o.onloadCalled||"function"!=typeof e.onload))&&o.end(e)}catch(n){try{f.emit("internal-error",[n])}catch(r){}}};for(var a=0;a