bugle
Version:
Hapi plugin for Google Drive Login with refresh token storage policy and drive API extensions from npm: decorated-google-drive
305 lines (277 loc) • 9.08 kB
JavaScript
// Copyright 2019- Paul Brewer, Economic and Financial Technology Consulting LLC <drpaulbrewer@eaftc.com>
// License: MIT
const fs = require('fs');
const driveX = require('decorated-google-drive');
const googleapis = require('googleapis').google;
const axios = require('axios');
const opener = require('open-from-google-drive');
const Iron = require('iron');
const Boom = require('boom');
const str = require('string-to-stream');
function bugle(server, options, next) {
const googleCred = server.registrations.grant.options.google; // requires Object { key, secret } -- other props ignored
if (options.openUrl) {
if (!options.hostname)
throw new Error("bugle: missing options.hostname");
const openCred = {
key: googleCred.key,
secret: googleCred.secret,
redirect: 'https://' + options.hostname + options.openUrl
};
const openSelectedDriveFile = opener(googleapis, openCred).open;
server.method('openSelectedDriveFile', openSelectedDriveFile);
}
const cached = {};
function cacheContent(what, path) {
fs.readFile(path, (err, content) => {
cached[what] = content;
});
}
cacheContent('login', options.loginHTMLFile || __dirname + '/html/loginWithGoogleDrive.html');
cacheContent('logout',options.logoutHTMLFile || __dirname + '/html/logout.html');
cacheContent('retry', options.retryHTMLFile || __dirname + '/html/retryWithGoogleDrive.html');
function getCachedPage(what) {
return function (req, reply) {
reply(cached[what]).type('text/html');
};
}
if ((!googleCred) || (typeof (googleCred.key) !== 'string') || (typeof (googleCred.secret) !== 'string'))
next(new Error("bugle: expected to find google credentials in grant module configuration"));
async function handleopen(req, reply) {
const params = req.query;
const fields = options.openfields || '*';
const maxSize = options.openmaxsize || 100 * 1024;
try {
const {
user,
drive,
file,
contents
} = await server.methods.openSelectedDriveFile({
params,
fields,
maxSize
});
if (typeof (server.methods.onopen) === 'function') {
await server.methods.onopen({
req,
reply,
user,
drive,
file,
contents
});
} else {
const out = JSON.stringify([user, file, contents], null, 2);
reply(out).type('text');
}
} catch (e) {
// eslint-disable-next-line no-console
console.dir(e);
}
}
function getTokensFromCookie(req) {
let tokens;
const cookieManager = req.session || req.yar;
if (cookieManager) {
const grantCookie = cookieManager.get('grant');
const bugleCookie = cookieManager.get('bugle');
// a grant cookie with a refresh token gets top priority because it is fresh from the Google OAuth2 endpoint
// a bugle cookie with a refresh token is an older refresh token and is also ok
// if neither cookie contains a refresh token, look for an access token in the grant cookie
// if that fails, nothing
if (grantCookie &&
grantCookie.response &&
grantCookie.response.refresh_token
) {
tokens = grantCookie.response;
} else if (bugleCookie && bugleCookie.refresh_token) {
tokens = bugleCookie;
} else if (grantCookie &&
grantCookie.response &&
grantCookie.response.access_token) {
tokens = grantCookie.response;
}
if (tokens && tokens._raw) delete tokens._raw;
if (tokens && tokens.raw) delete tokens.raw;
return tokens;
}
}
function spinupGoogleDrive(req, reply) {
const tokens = getTokensFromCookie(req);
if (tokens && (tokens.access_token)) {
req.drive = driveX({
google: googleapis,
axios,
keys: googleCred,
tokens,
salt: options.hexidSalt
});
}
reply.continue();
}
function retryDriveLogin() {
return this.redirect('/a/googledriveretry'); // jshint ignore:line
}
server.decorate('reply', 'retryDriveLogin', retryDriveLogin);
function hapiDriveConductor(req, reply) {
const tokens = getTokensFromCookie(req);
const drive = req.drive;
if (!drive) {
// not logged in
return reply.redirect('/a/googledriveretry');
}
let pRefresh;
if (options && options.drive && options.drive.refreshTokenStash &&
(options.drive.refreshTokenStash.location === 'appDataFolder')) {
const refreshFile = options.drive.refreshTokenStash.file;
const refreshIronKey = options.drive.refreshTokenStash.key;
if (tokens.refresh_token) {
pRefresh = (
Iron.seal(tokens.refresh_token, refreshIronKey, Iron.defaults)
.then((sealed) => (drive.x.appDataFolder.upload2({
folderPath: '',
name: refreshFile,
stream: str(sealed),
mimeType: 'text/plain',
clobber: true
})))
);
} else {
pRefresh = (
drive.x.appDataFolder.download(refreshFile)
.then((sealed) => (Iron.unseal(sealed, refreshIronKey, Iron.defaults)))
.then((refresh_token) => {
tokens.refresh_token = refresh_token;
})
);
}
} else {
pRefresh = Promise.resolve(); // do-nothing placeholder
}
(pRefresh
.then(
() => {
const cookieManager = (req.session || req.yar);
cookieManager.clear('grant');
cookieManager.set('bugle', tokens);
reply.redirect(options.myRedirect || '/a/me');
},
() => {
reply.redirect('/a/googledriveretry');
})
);
}
function driveUserProfile(req, reply) {
const drive = req.drive;
if (drive)
drive.x.aboutMe().then(reply).catch(() => {
reply(Boom.badData("no response from Google Drive"));
});
else reply.redirect('/a/googledriveretry').takeover();
}
function driveUpdateTokens(req, reply) {
if (req && req.drive) {
try {
const tokens = req.drive.x.auth.credentials;
if (tokens && tokens.access_token && tokens.refresh_token) {
const cookieSource = (req.session || req.yar);
const bugleCookie = cookieSource.get('bugle');
const old_access_token = bugleCookie && bugleCookie.access_token;
if (old_access_token !== tokens.access_token) {
cookieSource.set('bugle', tokens);
}
} else {
throw new Error("tokens, tokens.access_token, or tokens.refresh_token is undefined");
}
} catch (e) {
// eslint-disable-next-line no-console
console.log("driveUpdateTokens: " + e.toString());
}
// ignore errors here resetting cookie - worst case is the access_token never updates and googleapis uses the refresh_token every time to get an access_token
delete req.drive;
}
reply.continue();
}
function logout(req, reply) {
const cookieManager = (req.session || req.yar);
cookieManager.reset();
reply(cached.logout).type('text/html');
}
server.ext([{
type: 'onPreAuth',
method: spinupGoogleDrive
},
{
type: 'onPostHandler',
method: driveUpdateTokens
}
]);
if (options.openUrl) {
server.route({
method: 'GET',
path: options.openUrl,
handler: handleopen
});
}
server.route([{
method: 'GET',
path: googleCred.callback,
handler: hapiDriveConductor
},
{
method: 'GET',
path: '/a/login',
handler: getCachedPage('login')
},
{
method: 'GET',
path: '/a/logout',
handler: logout
},
{
method: 'GET',
path: '/a/googledriveretry',
handler: getCachedPage('retry')
},
{
method: 'GET',
path: '/a/googledrivereset',
handler: getCachedPage('retry')
},
{
method: 'GET',
path: '/a/me',
config: {
pre: [{
method: driveUserProfile,
assign: 'me',
failAction: 'error'
}],
handler: function (req, reply) {
let page = '';
const level = options.useMeLevel || 0;
if (level > 0) {
if (level > 0) page += '<h2>Welcome, ' + req.pre.me.user.displayName + '</h2>';
if (level > 0) page += '<img src="' + req.pre.me.user.photoLink + '" />';
if (level > 1) page += '<p>From Drive</p><pre>' + JSON.stringify(req.pre.me, null, 4) + '</pre>';
if (level > 2) page += '<p>From req.headers</p><pre>' + JSON.stringify(req.headers, null, 4) + '</pre>';
if (level > 3) page += '<p>From req.info</p><pre>' + JSON.stringify(req.info, null, 4) + '</pre>';
page += '<p><a href="/a/logout">Logout</a></p>';
reply(page);
} else {
reply(Boom.notFound());
}
}
}
}
]);
next(); // call next to complete plugin registration
}
bugle.attributes = {
pkg: require('./package.json'),
dependencies: 'grant'
};
module.exports = {
register: bugle
};