Skip to content
This repository was archived by the owner on Nov 2, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Elvanto API Node.js Library

> Version 1.0.2

This library is all set to go with version 1 of the <a href="https://www.elvanto.com/api/" target="_blank">Elvanto API</a>.

## Installation
Expand All @@ -14,10 +16,9 @@ npm install elvanto-api

The Elvanto API supports authentication using either <a href="https://www.elvanto.com/api/getting-started/#oauth" target="_blank">OAuth 2</a> or an <a href="https://www.elvanto.com/api/getting-started/#api_key" target="_blank">API key</a>.

### What is This For?
### What Is This For?

* This is an API wrapper to use in conjunction with an Elvanto account. This wrapper can be used by developers to develop programs for their own churches, or to design integrations to share to other churches using OAuth authentication.
* Version 1.0.0

### Using OAuth 2

Expand Down
158 changes: 78 additions & 80 deletions lib/client.js
Original file line number Diff line number Diff line change
@@ -1,93 +1,91 @@
var discoverError = require('./error.js');
var https = require("https");
var syncHttp = require("urllib-sync")
var querystring = require("querystring");
var utils = require('./utils.js');
const discoverError = require('./error.js')
const querystring = require('querystring')
const fetch = require('node-fetch')

var config = {
host: "api.elvanto.com",
port: 443,
method: "post",
headers: {}
const config = {
host: 'api.elvanto.com'
}

var options = null

// <overview> HTTP post request </overview>
var post = function(path, data, callback){
return callback ? request(path, data, callback) : syncRequest(path, data)
}

// <overview> Make an api call to Elvanto endpoint </overview>
// @param [String] resource Path to Elvanto endpoint
// @param [Object] data
// @param [Function] callback. If callback is not present, then call will be synchronous
// @return [Object] response body
var apiCall = function(resource, data, callback){
options = config;
options["headers"]["Content-Type"] = "application/json";
return post(resource, JSON.stringify(data || {}), callback)
class Client {
constructor (authData = {}) {
this.auth = null
this.options = {
headers: {
Accept: 'application/json'
}
}

this.configure(authData)
}

// @params [Object] params.
// When using OAuth authentication it is {accessToken: "accessToken"}
// When using an API key it is {apiKey: "apiKey"}
configure ({ accessToken, apiKey }) {
if (accessToken) {
this.auth = { accessToken }
this.options['headers']['Authorization'] = `Bearer ${accessToken}`
} else if (apiKey) {
this.auth = { apiKey }
let B64auth = Buffer.from(`${apiKey}:x`).toString('base64')
this.options['headers']['Authorization'] = `Basic ${B64auth}`
}

return this
}

// @param [String] endPoint for example: "people/getAll" or "groups/GetInfo"
// @param [Object] option List of parameters
// @return [Object] response body
apiCall (endPoint, data) {
if (!this.auth) {
throw new Error(
'Not configured - Please provide an access token or an API key'
)
}

let headers = { 'Content-Type': 'application/json' }

return post(
`https://${config.host}/v1/${endPoint}.json`,
JSON.stringify(data || {}),
{ ...this.options['headers'], headers }
)
}
}


// <overview> Retrieve tokens </overview>
// @param [Object] data
// @param [Function] callback. If callback is not present, then call will be synchronous
// @return [Object] {access_token: accessToken, expiresIn, refreshToken}
var retrieveTokens = function(data, callback){
options = config;
options["headers"]["Content-Type"] = "application/x-www-form-urlencoded";
return post("/oauth/token", querystring.stringify(data || {}), callback)
// @return [Object] {access_token, expires_in, refresh_token}
let retrieveTokens = function (data) {
let headers = { 'Content-Type': 'application/x-www-form-urlencoded' }

return post(
`https://${config.host}/oauth/token`,
querystring.stringify(data || {}),
headers
)
}


// <overview> Asynchronous request </overview>
var request = function(path, data, callback){
options["path"] = path;

var request = https.request(options, function (response) {
var payload = ""
response.on('data', function(chunk){
payload += chunk;
});
response.on('end', function() {
body = payload ? JSON.parse(payload) : {} ;

// Throws an exception if error found
discoverError(response.statusCode, body);

callback(body);
});
});
request.on('error', function (error) {
console.log(error.message);
});

request.write(data);
request.end();

return request;
}

// <overview> Synchronous request </overview>
var syncRequest = function(path, data, callback){
var url = utils.buildURL("https", options["host"], path);
options["data"] = data;
options["dataType"] = "json";
options["timeout"] = 20000;

var response = syncHttp.request(url, options);
var body = response.data;

// Throws an exception if error found
discoverError(response.statusCode, body);

return body;
const post = function (path, data, headers) {
return fetch(path, {
method: 'POST',
body: data,
headers
}).then(response =>
response.json().then(json => {
try {
discoverError(response.status, json)
} catch (err) {
throw err
}
return json
})
)
}

module.exports = {
config: config,
apiCall: apiCall,
retrieveTokens: retrieveTokens
Client,
config,
retrieveTokens
}

123 changes: 51 additions & 72 deletions lib/elvanto.js
Original file line number Diff line number Diff line change
@@ -1,95 +1,74 @@
// Node JS wrapper for Elvanto API.
// Can work in both asynchronous over synchronous mode
// Asynchronous Node.js wrapper for Elvanto API.

var utils = require('./utils.js');
var client = require('./client.js');

const DEFAULT_OPTIONS = {
apiVersion: "/v1/",
accept: "json"
};

var options = {};

// @params [Object] params. In case of Oauth athentication it is {accessToken: "accessToken"}.
// When authenticating with an API key it {apiKey: "apiKey"}
exports.configure = function(params){
options = utils.mergeOptions(DEFAULT_OPTIONS, params);

if (options["apiKey"]){
client.config["auth"] = options["apiKey"] + ':x'
}
else if (options["accessToken"]){
client.config["headers"] = {"Authorization": "Bearer " + options["accessToken"]}
}
else {
throw new Error('You should provide either access token or API key');
}

return client.config;
};
const client = require('./client.js')
const querystring = require('querystring')

// @params [String] clientId The Client ID of your registered OAuth application.
// @params [String] redirectUri The Redirect URI of your registered OAuth application.
// @params [String] scope
// @params [String] state Optional state data to be included in the URL.
// @return [String] The authorization URL to which users of your application should be redirected.
exports.authorizeUrl = function(clientId, redirectUri, scope, state){
if (scope instanceof Array){
scope = scope.join();
}
const authorizeUrl = function (clientId, redirectUri, scope, state) {
if (typeof clientId === 'undefined') throw new Error('clientId is required')
if (typeof redirectUri === 'undefined') throw new Error('redirectUri is required')

if (scope instanceof Array) {
scope = scope.join()
}

params = {type: "web_server", client_id: clientId, redirect_uri: redirectUri, scope: scope}
let params = {
type: 'web_server',
client_id: clientId,
redirect_uri: redirectUri,
scope: scope
}

if (state) {
params["state"] = state;
}
if (state) {
params['state'] = state
}

return utils.buildURL("https", client.config["host"], "oauth", params)
};
let url = `https://${client.config['host']}/oauth?${querystring.stringify(params)}`

return url
}

// @params [String] clientId The Client ID of your registered OAuth application.
// @param [String] clientSecret The Client Secret of your registered OAuth application.
// @param [String] code The unique OAuth code to be exchanged for an access token.
// @param [String] redirectUrl The Redirect URI of your registered OAuth application.
// @param [function] callback. If callback is not present, then call will be synchronous
// @return [Object] {access_token: accessToken, expiresIn, refreshToken}
exports.exchangeToken = function(clientId, clientSecret, code, redirectUri, callback){
data = {"grant_type": 'authorization_code', "client_id": clientId, "client_secret": clientSecret, "code": code, "redirect_uri": redirectUri};
return client.retrieveTokens(data, callback);
};
const exchangeToken = function (clientId, clientSecret, code, redirectUri) {
if (typeof clientId === 'undefined') throw new Error('clientId is required')
if (typeof clientSecret === 'undefined') throw new Error('clientSecret is required')
if (typeof code === 'undefined') throw new Error('code is required')
if (typeof redirectUri === 'undefined') throw new Error('redirectUri is required')

let data = {
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: redirectUri
}

return client.retrieveTokens(data)
}

// @param [String] refreshToken Was included when the original token was granted to automatically retrieve a new access token.
// @param [Function] callback. If callback is not present, then call will be synchronous
// @return [Object] {access_token: accessToken, expiresIn, refreshToken}
exports.refreshToken = function(refreshToken, callback){
if (typeof refreshToken === 'undefined')
throw new Error('Error refreshing token. There is no refresh token set on this object');
const refreshToken = function (refreshToken) {
if (typeof refreshToken === 'undefined') {
throw new Error('No refresh token given')
}

data = {grant_type: "refresh_token", refresh_token: refreshToken};
return client.retrieveTokens(data, callback);
}
let data = { grant_type: 'refresh_token', refresh_token: refreshToken }

// @param [String] resource
// @return [String] path to resource
var resourcePath = function(resource){
if (!options["apiVersion"] || !options["accept"]){
throw new Error('Most probably you forgot to call configure function');
}

return options["apiVersion"] + resource + "." + options["accept"]
};

// @param [String] endPoint for example: "people/getAll" or "groups/GetInfo"
// @param [Object] option List of parametrs
// @param [Function] callback for response body. If callback is not present, then call will be synchronous
// @return [Object] response body
exports.apiCall = function(endPoint, data, callback){
return client.apiCall(resourcePath(endPoint), data, callback);
return client.retrieveTokens(data)
}






module.exports = {
Client: client.Client,
authorizeUrl,
exchangeToken,
refreshToken
}
51 changes: 17 additions & 34 deletions lib/error.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
const CATEGORY_CODE_MAP = {
"50": "Elvanto::Unauthorized: ",
"100": "Elvanto::Unauthorized: ",
"102": "Elvanto::Unauthorized: ",
"250": "Elvanto::BadRequest: ",
"404": "Elvanto::NotFound: ",
"500": "Elvanto::InternalError: "
};
50: 'Elvanto::Unauthorized:',
100: 'Elvanto::Unauthorized:',
102: 'Elvanto::Unauthorized:',
250: 'Elvanto::BadRequest:'
}

const HTTP_STATUS_CODES = {
"401": "Elvanto::Unauthorized: ",
"400": "Elvanto::BadRequest: ",
"404": "Elvanto::NotFound: ",
"500": "Elvanto::InternalError: "
};

// <overview> Throws an exception if response has bad status code or response's body contains an error message. </overview>
var discoverError = function(status_code, body){
if (body.error_description){
handleError(status_code, body.error_description);
}
else if (body.error){
handleError(body.error.code, body.error.message);
}
return true;
401: 'Elvanto::Unauthorized:',
400: 'Elvanto::BadRequest:',
404: 'Elvanto::NotFound:',
500: 'Elvanto::InternalError:'
}

var handleError = function(code, message){
if (CATEGORY_CODE_MAP[code]){
throw new Error(CATEGORY_CODE_MAP[code] + message);
}
else if(HTTP_STATUS_CODES[code]){
throw new Error(HTTP_STATUS_CODES[code] + message);
}
else {
throw new Error(message);
}
};
let ERROR_CODES = { ...CATEGORY_CODE_MAP, ...HTTP_STATUS_CODES }

// <overview> Throws an exception if response has bad status code </overview>
const discoverError = function (code, resp) {
if (ERROR_CODES[code]) throw new Error(ERROR_CODES[code])
if (resp.error_description) throw new Error(`Elvanto::${resp.error}:${resp.error_description}`)
}

module.exports = discoverError;
module.exports = discoverError
Loading