Skip to content

Commit 98fbdde

Browse files
committed
Add way to make login to readme easy
1 parent bb4cad5 commit 98fbdde

11 files changed

+355
-19
lines changed

config/default.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"host": "https://metrics.readme.io",
3+
"readmeUrl": "http://dash.readme.local:3000",
34
"bufferLength": 10
45
}

index.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
const request = require('r2');
2+
const uuid = require('node-uuid');
3+
const jwt = require('jsonwebtoken');
24
const config = require('./config');
35

46
const constructPayload = require('./lib/construct-payload');
7+
const getReadmeData = require('./lib/get-readme-data');
58

69
// We're doing this to buffer up the response body
710
// so we can send it off to the metrics server
@@ -27,7 +30,7 @@ function patchResponse(res) {
2730
};
2831
}
2932

30-
module.exports = (apiKey, group, options = {}) => {
33+
module.exports.metrics = (apiKey, group, options = {}) => {
3134
if (!apiKey) throw new Error('You must provide your ReadMe API key');
3235
if (!group) throw new Error('You must provide a grouping function');
3336

@@ -72,3 +75,43 @@ module.exports = (apiKey, group, options = {}) => {
7275
return next();
7376
};
7477
};
78+
79+
module.exports.login = (apiKey, userFnc, options = {}) => {
80+
if (!apiKey) throw new Error('You must provide your ReadMe API key');
81+
if (!userFnc) throw new Error('You must provide a function to get the user');
82+
return async (req, res) => {
83+
let u;
84+
try {
85+
u = userFnc(req);
86+
} catch (e) {
87+
// User isn't logged in
88+
}
89+
90+
if (!u) {
91+
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
92+
return res.redirect(`${options.loginUrl}?redirect=${encodeURIComponent(fullUrl)}`);
93+
}
94+
95+
const jwtUrl = await module.exports.magicLink(apiKey, u, req.query.redirect);
96+
return res.redirect(jwtUrl);
97+
};
98+
};
99+
100+
module.exports.magicLink = async (apiKey, user, redirectPath = '') => {
101+
if (!apiKey) throw new Error('You must provide your ReadMe API key');
102+
if (!user) throw new Error('You must provide a user object');
103+
104+
const readmeData = await getReadmeData(apiKey);
105+
let baseUrl = redirectPath;
106+
107+
if (!redirectPath.startsWith('http')) {
108+
baseUrl = `${readmeData.baseUrl}${redirectPath}`;
109+
}
110+
111+
const jwtOptions = {
112+
jwtid: uuid.v4(),
113+
};
114+
115+
const token = jwt.sign(user, readmeData.jwtSecret, jwtOptions);
116+
return `${baseUrl}?auth_token=${token}`;
117+
};

lib/construct-payload.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const url = require('url');
12
const processRequest = require('./process-request');
23
const processResponse = require('./process-response');
34
const { name, version } = require('../package.json');
@@ -10,7 +11,13 @@ module.exports = (req, res, group, options, { startedDateTime }) => ({
1011
creator: { name, version, comment: `${process.platform}/${process.version}` },
1112
entries: [
1213
{
13-
pageref: req.route ? req.route.path : '',
14+
pageref: req.route
15+
? url.format({
16+
protocol: req.protocol,
17+
host: req.get('host'),
18+
pathname: req.route.path,
19+
})
20+
: '',
1421
startedDateTime: startedDateTime.toISOString(),
1522
time: new Date().getTime() - startedDateTime.getTime(),
1623
request: processRequest(req, options),

lib/get-readme-data.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const LRU = require('lru-cache');
2+
const request = require('r2');
3+
const config = require('../config');
4+
5+
const cacheOptions = {
6+
length: 500,
7+
maxAge: 24 * 60 * 60 * 1000, // 24 Hours
8+
};
9+
10+
module.exports = async apiKey => {
11+
let readmeData = module.exports.cache.get(apiKey);
12+
13+
if (!readmeData) {
14+
const encoded = Buffer.from(`${apiKey}:`).toString('base64');
15+
readmeData = await request.get(`${config.readmeUrl}/api/v1/jwt-secret`, {
16+
headers: { authorization: `Basic ${encoded}` },
17+
}).json;
18+
module.exports.cache.set(apiKey, readmeData);
19+
}
20+
21+
return readmeData;
22+
};
23+
24+
module.exports.cache = LRU(cacheOptions);

lib/process-request.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module.exports = (req, options = {}) => {
1717
method: req.method,
1818
url: url.format({
1919
protocol: req.protocol,
20-
host: req.get('host'),
20+
host: req.headers['x-forwarded-host'] || req.get('host'),
2121
pathname: req.path,
2222
query: req.query,
2323
}),

package-lock.json

+98-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
"version": "0.0.0",
55
"dependencies": {
66
"configly": "^4.1.0",
7+
"jsonwebtoken": "^8.3.0",
78
"lodash.omit": "^4.5.0",
89
"lodash.pick": "^4.4.0",
10+
"lru-cache": "^4.1.3",
11+
"node-uuid": "^1.4.8",
912
"r2": "^2.0.1"
1013
},
1114
"scripts": {

test/construct-payload.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('constructPayload()', () => {
5757
request(createApp({}))
5858
.post('/')
5959
.expect(({ body }) => {
60-
assert.equal(body.request.log.entries[0].pageref, '/*');
60+
assert(body.request.log.entries[0].pageref.endsWith('/*'));
6161
}));
6262

6363
it('#startedDateTime', () => {

test/get-readme-data.test.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* eslint-env mocha */
2+
const assert = require('assert');
3+
const nock = require('nock');
4+
const config = require('../config');
5+
const getReadmeData = require('../lib/get-readme-data');
6+
7+
describe('#get-readme-data', () => {
8+
before(() => {
9+
nock.disableNetConnect();
10+
nock.enableNetConnect('127.0.0.1');
11+
});
12+
after(() => nock.cleanAll());
13+
14+
beforeEach(() => getReadmeData.cache.reset());
15+
16+
beforeEach(() => {
17+
nock(config.readmeUrl)
18+
.get('/api/v1/jwt-secret')
19+
.basicAuth({
20+
user: 'readme_api',
21+
pass: '',
22+
})
23+
.reply(200, { jwtSecret: 'jwt', baseUrl: 'redirect' });
24+
});
25+
26+
it('should send get jwt secret and redirect from readme', async function test() {
27+
this.timeout(5000);
28+
29+
const data = await getReadmeData('readme_api');
30+
assert.deepEqual(data, { jwtSecret: 'jwt', baseUrl: 'redirect' });
31+
});
32+
33+
it('should cache if called twice', async () => {
34+
await getReadmeData('readme_api');
35+
const data = await getReadmeData('readme_api');
36+
assert.deepEqual(data, { jwtSecret: 'jwt', baseUrl: 'redirect' });
37+
assert.deepEqual(getReadmeData.cache.get('readme_api'), {
38+
jwtSecret: 'jwt',
39+
baseUrl: 'redirect',
40+
});
41+
});
42+
});

0 commit comments

Comments
 (0)