akima-client
Version:
Client side support for the Akima oauth2 framework
410 lines (357 loc) • 11.1 kB
JavaScript
;
const Redis = require("ioredis");
const memoize = require("memoizee");
const pg = require('pg');
let redisClient = null;
let pgConString = null;
let pgUtil;
let getUserRouter = {};
const maxAge = 2000;
const immutableCallMaxAge = 60000;
function initialize(config) {
if (config.type == "redis") redisClient = Redis.createClient(config.port, config.host, {detect_buffers: true});
else {
pgConString = config.pgConString;
pgUtil = require("./pg_util")(pgConString);
doPGSubscribe('token_modified',newPGSubscriberContext(pgConString),function(msg) {
let tokenId = msg.payload;
memoizedPGGetToken.delete(tokenId);
});
}
}
/****
Copied from akima server pg-based pub/sub fault tolerance. The idea is that the database
connection is allowed to break, but the client will continue to re-attempt if so. Do
exponential backoff to make it efficient, and max out at 8s re-query to make it react
quickly when the connection becomes available. This can operate in parallel with the pg
store as it uses a separate and independant client.
****/
function newPGSubscriberContext(connectionString,backoff) {
if(backoff == null) backoff = 500;
return { subscriber:new pg.Client(connectionString), connectionString:connectionString, backoff:backoff };
}
function handlePGSubscriberError(toChannel,withContext,error,callback) {
var backoff = 2*withContext.backoff;
console.log('subscription error ',error);
withContext.subscriber.end();
if(backoff > 8000) backoff = 8000;
setTimeout(function() {
var newContext = newPGSubscriberContext(withContext.connectionString,backoff);
doPGSubscribe(toChannel,newContext,callback);
}, backoff);
}
function doPGSubscribe(toChannel,withContext,callback) {
withContext.subscriber.connect(function(err) {
if(err) handlePGSubscriberError(toChannel,withContext,err,callback);
else {
withContext.subscriber.query("LISTEN " + toChannel,function(err) {
if(err) handlePGSubscriberError(toChannel,withContext,err,callback);
else {
withContext.subscriber.on('notification',function(msg) { callback(msg); });
withContext.subscriber.on('error',function(error) {
handlePGSubscriberError(toChannel,withContext,error,callback);
});
}
});
}
});
}
function redisGetToken(tokenId, callback) {
redisClient.get(tokenId, function (err, reply) {
if (err) {
console.log("Error fetching token information from redis: " + err);
return callback(err);
}
if (reply) {
let token = JSON.parse(reply.toString());
if (token) {
token.id = tokenId;
}
callback(null, token);
}
else {
callback(null, null);
}
});
}
function pgGetToken(tokenId, callback) {
//console.log("************************** real get token *******************************");
pgUtil.performSingleRowQuery(
{
"query": "SELECT info FROM tokens WHERE token_id=$1",
"queryParameters": [tokenId],
"callback": function (err, obj) {
if (err) {
return callback(err);
}
let token = null;
if (obj) {
if (typeof(obj.info) === "string") token = JSON.parse(obj.info);
else token = obj.info;
token.id = tokenId;
}
callback(null, token);
}
});
}
const memoizedRedisGetToken = memoize(redisGetToken,{ async:true, maxAge: immutableCallMaxAge });
const memoizedPGGetToken = memoize(pgGetToken,{ async:true, maxAge: immutableCallMaxAge });
function redisSaveToken(tokenId, params, user, callback) {
if (tokenId) {
memoizedRedisGetToken.delete(tokenId);
let v = {"params": params, "user": user};
redisClient.set(tokenId, JSON.stringify(v));
}
callback();
}
function pgSaveToken(tokenId, params, user, callback) {
if (tokenId) {
memoizedPGGetToken.delete(tokenId);
let o = {tokenId: tokenId, info: JSON.stringify({"params": params, "user": user})};
pgUtil.performObjectSave(
{
"tableName": "tokens",
"primaryKeyName": "token_id",
"object": o,
"callback": callback
});
}
else {
callback();
}
}
function pgUpsertToken(tokenId,params,user,callback) {
let query;
let o = { params:params, user:user };
memoizedPGGetToken.delete(tokenId);
query = `
WITH upsert AS
(
select
$1::text AS token_id,
$2::json AS info,
$3::timestamp AS expires_at
),
update_option AS
(
UPDATE
tokens SET info = upsert.info,
expires_at = upsert.expires_at FROM upsert
WHERE tokens.token_id = upsert.token_id
)
INSERT INTO tokens (token_id,info,expires_at) SELECT
token_id,
info,
expires_at
FROM upsert
WHERE NOT EXISTS(SELECT 1 FROM tokens WHERE tokens.token_id = upsert.token_id)`
pgUtil.performSingleRowQuery(
{
"query": query,
"queryParameters": [tokenId,o,params.expiresAt],
"callback": function(err) {
if(typeof err == 'string' && err.match(/duplicate/) != null) callback();
else callback(err);
}
});
}
function redisRemoveToken(tokenId, callback) {
if (tokenId) {
memoizedRedisGetToken.delete(tokenId);
redisClient.del(tokenId);
}
callback();
}
function pgRemoveToken(tokenId, callback) {
if (tokenId) {
memoizedPGGetToken.delete(tokenId);
pgUtil.performObjectDelete(
{
tableName: "tokens",
key: "token_id",
id: tokenId,
"callback": callback
});
}
else {
callback();
}
}
function redisGetUser(userId, callback) {
redisClient.get(userId, function (err, reply) {
if (err) {
return callback(err);
}
if (reply) {
user = JSON.parse(reply.toString());
callback(null, user);
}
else {
callback(null, null);
}
});
}
function pgGetUser(userId, callback) {
pgUtil.performSingleRowQuery(
{
"query": "SELECT info FROM users WHERE user_id=$1",
"queryParameters": [userId],
"callback": function (err, obj) {
if (err) {
return callback(err);
}
let user = null;
if (obj) {
if (typeof(obj) === "string") {
user = JSON.parse(obj.info);
}
else user = obj.info;
}
callback(null, user);
}
});
}
const memoizedRedisGetUser = memoize(redisGetUser,{ async:true, maxAge: maxAge });
const memoizedPGGetUser = memoize(pgGetUser,{ async:true, maxAge: maxAge });
function pgUpsertUser(userId,info,callback) {
let query;
memoizedPGGetUser.delete(userId);
query = `
WITH upsert AS
(
SELECT $1::text AS user_id,
$2::json AS info
),
update_option AS
(
UPDATE users SET info = upsert.info
FROM upsert
WHERE users.user_id = upsert.user_id
)
INSERT INTO users (user_id,info) SELECT
user_id,info
FROM upsert
WHERE NOT EXISTS(SELECT 1 FROM users WHERE users.user_id = upsert.user_id)`
pgUtil.performSingleRowQuery(
{
"query": query,
"queryParameters": [userId,info],
"callback": function(err) {
if(typeof err == 'string' && err.match(/duplicate/) != null) callback();
else callback(err);
}
});
}
function redisUpsertTokenAndUser(tokenId,tokenInfo,profile,callback) {
if (userId != null && profile.id != null) {
let userId = profile.id;
let v = {"params": params, "user": user};
if(memoizedRedisGetToken != null) memoizedRedisGetToken.delete(tokenId);
if(memoizedRedisGetUser != null) memoizedRedisGetUser.delete(userId);
redisClient.set(tokenId,JSON.stringify(v));
redisClient.set(userId,JSON.stringify(user));
}
}
function pgUpsertTokenAndUser(tokenId,tokenInfo,userInfo,callback) {
let query = "SELECT upsert_token_and_user($1::text,$2::json,$3::text,$4::json)";
let o = { params:tokenInfo, user:userInfo };
pgUtil.performSingleRowQuery(
{
"query": query,
"queryParameters": [tokenId,o,userInfo.id,userInfo],
"callback": function(err) {
if(err) callback(err);
else callback();
}
});
}
function redisSaveUser(userId, user, callback) {
if (userId) {
memoizedRedisGetUser.delete(userId);
redisClient.set(userId, JSON.stringify(user));
}
callback();
}
function pgSaveUser(userId, user, callback) {
if (userId) {
let o = {userId: userId, info: JSON.stringify(user)};
memoizedPGGetUser.delete(userId);
pgUtil.performObjectSave(
{
"tableName": "users",
"primaryKeyName": "user_id",
"object": o,
"callback": callback
});
}
else {
callback();
}
}
function redisRemoveUser(userId, callback) {
if (userId) {
memoizedRedisGetUser.delete(userId);
redisClient.del(userId);
}
callback();
}
function pgRemoveUser(userId, callback) {
if (userId) {
memoizedPGGetUser.delete(userId);
pgUtil.performObjectDelete(
{
tableName: "users",
key: "user_id",
id: userId,
"callback": callback
});
}
else {
callback();
}
}
function getUserRoutingThunk(getUser) {
return function(userId,done) {
if(getUserRouter[userId] == null) {
getUserRouter[userId] = [done];
getUser(userId,function(err,user) {
let callbacks = getUserRouter[userId];
getUserRouter[userId] = null;
if(user == null) console.log(`user data for userId ${userId} is null, num callbacks = ${callbacks.length}`);
for(let i = 0;i < callbacks.length;i++) callbacks[i](err,user);
});
}
else getUserRouter[userId].push(done);
}
}
module.exports =
{
initialize: initialize,
redis: {
saveToken: memoize(redisSaveToken,{ async:true, maxAge: maxAge }),
getToken: memoizedRedisGetToken,
upsertToken: memoize(redisSaveToken,{ async:true, maxAge: maxAge }),
removeToken: memoize(redisRemoveToken,{ async:true, maxAge: maxAge }),
saveUser: memoize(redisSaveUser,{ async:true, maxAge: maxAge }),
getUser: getUserRoutingThunk(memoizedRedisGetUser),
upsertUser: memoize(redisSaveUser,{ async:true, maxAge: maxAge }),
removeUser: memoize(redisRemoveUser,{ async:true, maxAge: maxAge }),
upsertTokenAndUser: memoize(redisUpsertTokenAndUser,{ async:true, maxAge: maxAge })
},
pg: {
saveToken: memoize(pgSaveToken,{ async:true, maxAge: maxAge }),
getToken: function(token,done) {
memoizedPGGetToken(token,function(err,res) {
if(res == null) memoizedPGGetToken.delete(token);
done(err,res);
});
},
upsertToken: memoize(pgUpsertToken,{ async:true, maxAge: maxAge }),
removeToken: memoize(pgRemoveToken,{ async:true, maxAge: maxAge }),
saveUser: memoize(pgSaveUser,{ async:true, maxAge: maxAge }),
getUser: getUserRoutingThunk(memoizedPGGetUser),
upsertUser: memoize(pgUpsertUser,{ async:true, maxAge: maxAge }),
removeUser: memoize(pgRemoveUser,{ async:true, maxAge: maxAge }),
upsertTokenAndUser: memoize(pgUpsertTokenAndUser,{ async:true, maxAge: maxAge })
}
};