routes/api/api.js

/**
 * @module routes/api/api
 * @author jesse reichler <mouser@donationcoder.com>
 * @copyright 10/28/19
 * @description
 * ##### Overview
 * This file handles all requests related to the programmatic API interface for accessing the system.
 * These routes are all intended to be called programmatically by other code, and so should all return json replies.
*/

"use strict";


// modules
const express = require("express");


// requirement service locator
const jrequire = require("../../helpers/jrequire");

// controllers
const arserver = jrequire("arserver");

// helpers
const JrContext = require("../../helpers/jrcontext");
const jrhExpress = require("../../helpers/jrh_express");
const jrdebug = require("../../helpers/jrdebug");










//---------------------------------------------------------------------------
/**
 * Add the API routes
 *
 * @param {string} urlPath - the base path of these relative paths
 * @returns router object
 */
function setupRouter(urlPath) {
	// create express router
	const router = express.Router();

	// setup routes
	router.get("/", routerGetIndex);
	router.all("/reqrefreshsession", routerAllReqRefreshSession);
	router.get("/reqrefreshcredentials", routerGetReqRefreshCredentials);
	router.post("/reqrefreshcredentials", routerPostReqRefreshCredentials);
	router.all("/refreshaccess", routerAllRefreshAccess);
	router.all("/tokentest", routerAllTokenTest);
	router.all("/dos", routerAllDos);
	router.all("/hello", routerAllHello);

	// return router
	return router;
}
//---------------------------------------------------------------------------












//---------------------------------------------------------------------------
// router functions

/**
 * @description
 * Handle the request for the api index page,
 *  which currently just shows a web page index of links to all of the api functions.
 * @todo Replace the template with some json reply, since api should only be machine callable.
 */
async function routerGetIndex(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);
	// just show index
	res.render("api/index", {
		jrResult: jrContext.mergeSessionMessages(),
	});
}



/**
 * @description
 * Test function, and just complains and checks for rate limiting.
 * ##### NOTES
 * * Currently this function is just used to test rate limiting for DOS type attacks.
 */
// test rate limiting of generic 404s at api route
async function routerAllDos(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);
	// ATTN: test of rate limiting block
	const rateLimiter = arserver.getRateLimiterApi();
	// ATTN: NOTE that this is a PER-IP rate limit since we use rateLimiterKey = req.ip
	const rateLimiterKey = req.ip;
	//
	try {
		// ATTN: NOTE that this is a PER-IP rate limit since we use rateLimiterKey = req.ip
		await rateLimiter.consume(rateLimiterKey, 1);
	} catch (rateLimiterRes) {
		// rate limiter triggered
		jrhExpress.sendJsonError(jrContext, 429, "API rate limiting triggered; your ip has been blocked for " + (rateLimiter.blockDuration).toString() + " seconds.", "rateLimit");
		// exit from function
		return;
	}

	jrhExpress.sendJsonError(jrContext, 404, "API Route " + req.baseUrl + "/" + req.path + " not found.  API not implemented yet.", "404");
}



/**
 * @description
 * Present user with form for their username and password,
 * so they may request a long-lived Refresh token (JWT).
 * ##### NOTES
 * This route returns an html page (not json) and is used for user to fill in their credentials interactively; it may be unneeded since we expected api to be submitted programatically
 */
async function routerGetReqRefreshCredentials(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);
	// render page
	res.render("api/tokenuserpassform", {
	});
}
//---------------------------------------------------------------------------




//---------------------------------------------------------------------------
/**
 * @description
 * This uses the user's current logged in session to generate a refresh token.
 * It might be preferable to having user manually authenticate their credentials to get one because it would allow them to log in via facebook, twitter, etc.
 *
 * @param {*} req
 * @param {*} res
 * @param {*} next
 */
async function routerAllReqRefreshSession(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);

	if (!await arserver.requireRecentLoggedIn(jrContext, 60000)) {
		// errror and redirect will have happened
		return;
	}
	const user = await arserver.lookupLoggedInUser(jrContext);

	// success
	await renderRefreshTokenForUser(jrContext, user);
}
//---------------------------------------------------------------------------













//---------------------------------------------------------------------------
/**
 * @description
 * Process request for a long-lived Refresh token (JWT), after checking user's username and password in post data.
 * If username and password match, they will be issued a JWT refresh token that they can use to generate short-lived access tokens.
 * ##### Notes
 * * The IDEA is that the refresh token is coded with scope "api" and cannot be used to perform arbitrary actions on the site; it can only be used for api-like actions.
 * * The refresh token should only be used to request access tokens, which are short-lived tokens that can be use to perform actual api functions.
 */
async function routerPostReqRefreshCredentials(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);
	// do a local test of passed username and password; DON'T store session info (i.e. don't actually log them in just look up user if they match password)
	// The reason we do this like this instead of trusting a logged in session user is so that this call can be made by a client that does not ever do session login
	const user = await arserver.asyncPassportManualNonSessionAuthenticateGetUser(jrContext, "local", "with username and password", next);
	if (jrContext.isError()) {
		arserver.renderErrorJson(jrContext, 401);
		return;
	}

	// success
	await renderRefreshTokenForUser(jrContext, user);
}


async function renderRefreshTokenForUser(jrContext, user) {
	const secureToken = await arserver.makeSecureTokenRefresh(user);

	// log request
	arserver.logr(jrContext, "api.token", "generated refresh token", null, user);

	// provide it
	jrhExpress.sendJsonDataSuccess(jrContext, "token generated", secureToken);
}



/**
 * @description
 * Make a short-lived (JWT) Access token, using a Refresh token.  Here the user passes us a Refresh token and we give them (after verifying it's validity) an Access token.
 * ##### NOTES
 * * see <a href="https://scotch.io/@devGson/api-authentication-with-json-web-tokensjwt-and-passport">using refresh tokens with jwt</a>
 */
async function routerAllRefreshAccess(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);

	// first the user has to give us a valid REFRESH token
	const [userPassport, user] = await arserver.asyncPassportManualNonSessionAuthenticateFromTokenInRequestGetPassportProfileAndUser(jrContext, next, "refresh");
	if (jrContext.isError()) {
		arserver.renderErrorJson(jrContext, 403);
		return;
	}

	// ok they gave us a valid refresh token, so now we generate an access token for them
	const secureToken = await arserver.makeSecureTokenAccessFromRefreshToken(user, userPassport.token);

	// log request
	arserver.logr(jrContext, "api.token", "refreshed access token", null, user);

	// provide it
	jrhExpress.sendJsonDataSuccess(jrContext, "token generated", secureToken);
}



/**
 * @description
 * Evaluate a refresh or access token, and report on its contents and validity; useful for testing.
 * @todo This should probably not be present in production version.
 */
async function routerAllTokenTest(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);

	// retrieve/test the token passed by the user
	const userPassport = await arserver.asyncPassportManualNonSessionAuthenticateFromTokenInRequestGetMinimalPassportUsrData(jrContext, next, null);
	if (jrContext.isError()) {
		arserver.renderErrorJson(jrContext, 403);
		return;
	}

	// it's good
	// show them the userPassport data which will include .token
	const returnData = {
		token: userPassport.token,
	};
	jrhExpress.sendJsonDataSuccess(jrContext, "Valid token parsed in API test", returnData);
}
//---------------------------------------------------------------------------



//---------------------------------------------------------------------------
/**
 * @description
 * Just reply with a simple success message that a client could test for
 */
async function routerAllHello(req, res, next) {
	const jrContext = JrContext.makeNew(req, res, next);
	const returnData = {
		message: "hello world.",
		libVersion: arserver.getVersionLib(),
		apiVersion: arserver.getVersionApi(),
	};
	jrhExpress.sendJsonDataSuccess(jrContext, "Ok", returnData);
}
//---------------------------------------------------------------------------















module.exports = {
	setupRouter,
};