UNPKG

dce-expresskit

Version:

Shared functions, helpers, and tools for Harvard DCE Express-based servers

284 lines (255 loc) 9.06 kB
// Import express import express from 'express'; // Import dce-mango import { Collection } from 'dce-mango'; // Import dce-reactkit import { ParamType, LogFunction, LOG_ROUTE_PATH, LOG_REVIEW_STATUS_ROUTE, Log, LOG_REVIEW_GET_LOGS_ROUTE, ErrorWithCode, } from 'dce-reactkit'; // Import shared helpers import genRouteHandler from './genRouteHandler'; import getLogReviewerLogs from './getLogReviewerLogs'; // Import shared types import ExpressKitErrorCode from '../types/ExpressKitErrorCode'; import CrossServerCredential from '../types/CrossServerCredential'; // Stored copy of dce-mango log collection let _logCollection: Collection<Log>; // Stored copy of dce-mango cross-server credential collection let _crossServerCredentialCollection: Collection<CrossServerCredential>; /*------------------------------------------------------------------------*/ /* Helpers */ /*------------------------------------------------------------------------*/ /** * Get log collection * @author Gabe Abrams * @returns log collection if one was included during launch or null if we don't * have a log collection (yet) */ export const internalGetLogCollection = () => { return _logCollection ?? null; }; /** * Get cross-server credential collection * @author Gabe Abrams * @return cross-server credential collection if one was included during launch or null * if we don't have a cross-server credential collection (yet) */ export const internalGetCrossServerCredentialCollection = () => { return _crossServerCredentialCollection ?? null; }; /*------------------------------------------------------------------------*/ /* Main */ /*------------------------------------------------------------------------*/ /** * Prepare dce-reactkit to run on the server * @author Gabe Abrams * @param opts object containing all arguments * @param opts.app express app from inside of the postprocessor function that * we will add routes to * @param opts.getLaunchInfo CACCL LTI's get launch info function * @param [opts.logCollection] mongo collection from dce-mango to use for * storing logs. If none is included, logs are written to the console * @param [opts.logReviewAdmins=all] info on which admins can review * logs from the client. If not included, all Canvas admins are allowed to * review logs. If null, no Canvas admins are allowed to review logs. * If an array of Canvas userIds (numbers), only Canvas admins with those * userIds are allowed to review logs. If a dce-mango collection, only * Canvas admins with entries in that collection ({ userId, ...}) are allowed * to review logs * @param [opts.crossServerCredentialCollection] mongo collection from dce-mango to use for * storing cross-server credentials. If none is included, cross-server credentials * are not supported */ const initServer = ( opts: { app: express.Application, logReviewAdmins?: (number[] | Collection<any>), logCollection?: Collection<Log>, crossServerCredentialCollection?: Collection<CrossServerCredential>, }, ) => { _logCollection = opts.logCollection; _crossServerCredentialCollection = opts.crossServerCredentialCollection; /*----------------------------------------*/ /* Logging */ /*----------------------------------------*/ /** * Log an event * @author Gabe Abrams * @param {string} context Context of the event (each app determines how to * organize its contexts) * @param {string} subcontext Subcontext of the event (each app determines * how to organize its subcontexts) * @param {string} tags stringified list of tags that apply to this action * (each app determines tag usage) * @param {string} metadata stringified object containing optional custom metadata * @param {string} level log level * @param {string} [errorMessage] error message if type is an error * @param {string} [errorCode] error code if type is an error * @param {string} [errorStack] error stack if type is an error * @param {string} [target] Target of the action (each app determines the list * of targets) These are usually buttons, panels, elements, etc. * @param {LogAction} [action] the type of action performed on the target * @returns {Log} */ opts.app.post( LOG_ROUTE_PATH, genRouteHandler({ paramTypes: { context: ParamType.String, subcontext: ParamType.String, tags: ParamType.JSON, level: ParamType.String, metadata: ParamType.JSON, errorMessage: ParamType.StringOptional, errorCode: ParamType.StringOptional, errorStack: ParamType.StringOptional, target: ParamType.StringOptional, action: ParamType.StringOptional, }, handler: ({ params, logServerEvent }) => { // Create log info const logInfo: Parameters<LogFunction>[0] = ( (params.errorMessage || params.errorCode || params.errorStack) // Error ? { context: params.context, subcontext: params.subcontext, tags: params.tags, level: params.level, metadata: params.metadata, error: { message: params.errorMessage, code: params.errorCode, stack: params.errorStack, }, } // Action : { context: params.context, subcontext: params.subcontext, tags: params.tags, level: params.level, metadata: params.metadata, target: params.target, action: params.action, } ); // Add hidden boolean to change source to "client" const logInfoForcedFromClient = { ...logInfo, overrideAsClientEvent: true, }; // Write the log const log = logServerEvent(logInfoForcedFromClient); // Return return log; }, }), ); /*----------------------------------------*/ /* Log Reviewer */ /*----------------------------------------*/ /** * Check if a given user is allowed to review logs * @author Gabe Abrams * @param userId the id of the user * @param isAdmin if true, the user is an admin * @returns true if the user can review logs */ const canReviewLogs = async ( userId: number, isAdmin: boolean, ): Promise<boolean> => { // Immediately deny access if user is not an admin if (!isAdmin) { return false; } // If all admins are allowed, we're done if (!opts.logReviewAdmins) { return true; } // Do a dynamic check try { // Array of userIds if (Array.isArray(opts.logReviewAdmins)) { return opts.logReviewAdmins.some((allowedId) => { return (userId === allowedId); }); } // Must be a collection const matches = await opts.logReviewAdmins.find({ userId }); // Make sure at least one entry matches return matches.length > 0; } catch (err) { // If an error occurred, simply return false return false; } }; /** * Check if the current user has access to logs * @author Gabe Abrams * @returns {boolean} true if user has access */ opts.app.get( LOG_REVIEW_STATUS_ROUTE, genRouteHandler({ handler: async ({ params }) => { const { userId, isAdmin } = params; const canReview = await canReviewLogs(userId, isAdmin); return canReview; }, }), ); /** * Get filtered logs based on provided filters * @author Gabe Abrams, Yuen Ler Chow * @param pageNumber the page number to get * @param filters the filters to apply to the logs * @returns {Log[]} list of logs that match the filters */ opts.app.get( LOG_REVIEW_GET_LOGS_ROUTE, genRouteHandler({ paramTypes: { pageNumber: ParamType.Int, filters: ParamType.JSON, countDocuments: ParamType.Boolean, }, handler: async ({ params }) => { // Destructure params const { pageNumber, userId, isAdmin, filters, countDocuments, } = params; // Validate user const canReview = await canReviewLogs(userId, isAdmin); if (!canReview) { throw new ErrorWithCode( 'You cannot access this resource because you do not have the appropriate permissions.', ExpressKitErrorCode.NotAllowedToReviewLogs, ); } // Get logs const response = await getLogReviewerLogs({ pageNumber, filters, countDocuments, logCollection: _logCollection, }); // Return response return response; }, }), ); }; export default initServer;