caccl-lti
Version:
LTI launch validator for IMS-LTI standard launches.
283 lines (257 loc) • 8.89 kB
text/typescript
// Import libs
import express from 'express';
// Import shared types
import LaunchInfo from './shared/types/LaunchInfo';
import LaunchType from './shared/types/LaunchType';
import AssignmentDescription from './shared/types/AssignmentDescription';
import OutcomeDescription from './shared/types/OutcomeDescription';
// Check if this is a dev environment
const thisIsDevEnvironment = (process.env.NODE_ENV === 'development');
/*------------------------------------------------------------------------*/
/* Augment Session */
/*------------------------------------------------------------------------*/
declare module 'express-session' {
interface SessionData {
launchInfo: LaunchInfo,
authInfo: unknown,
selfLaunchState: any,
}
}
/*------------------------------------------------------------------------*/
/* Constants */
/*------------------------------------------------------------------------*/
// List of Canvas custom param names
const CANVAS_CUSTOM_PARAMS = [
'custom_canvas_api_domain',
'custom_canvas_course_id',
'custom_canvas_enrollment_state',
'custom_canvas_user_id',
'custom_canvas_user_login_id',
'custom_canvas_workflow_state',
'custom_canvas_assignment_title',
'custom_canvas_assignment_points_possible',
'custom_canvas_assignment_id',
];
/*------------------------------------------------------------------------*/
/* Main */
/*------------------------------------------------------------------------*/
/**
* Parses an LTI launch body and saves results to the session under
* req.session.launched (set to true) and req.session.launchInfo (contains
* all launch information...see /docs/LaunchInfo.md for more info)
* @author Gabe Abrams
* @param req express request instance
*/
const parseLaunch = async (req: express.Request) => {
/*----------------------------------------*/
/* Parse Launch */
/*----------------------------------------*/
// Extract body
const launchBody = (req as any).body;
// Detect launch type
const wasAssignmentLaunch = (
launchBody.custom_canvas_assignment_id
&& launchBody.custom_canvas_assignment_title
&& launchBody.custom_canvas_assignment_points_possible
);
const launchType = (
wasAssignmentLaunch
? LaunchType.Assignment
: LaunchType.Navigation
);
// Parse assignment launch
let assignment: AssignmentDescription;
let outcome: OutcomeDescription;
if (wasAssignmentLaunch) {
// Parse custom params
assignment = {
id: Number.parseInt(launchBody.custom_canvas_assignment_id),
name: String(launchBody.custom_canvas_assignment_title),
pointsPossible: Number.parseInt(
launchBody.custom_canvas_assignment_points_possible
),
};
// Parse outcomes
const acceptedDataValues = (
launchBody.ext_outcome_data_values_accepted.split(',')
);
outcome = {
url: String(launchBody.lis_outcome_service_url),
sourcedId: String(launchBody.lis_result_sourcedid),
urlSubmissionAccepted: !!acceptedDataValues.includes('url'),
textSubmissionAccepted: !!acceptedDataValues.includes('text'),
totalScoreAccepted: !!(
launchBody.ext_outcome_result_total_score_accepted
&& String(launchBody.ext_outcome_result_total_score_accepted) === 'true'
),
submittedAtAccepted: (
launchBody.ext_outcome_submission_submitted_at_accepted
&& (
String(launchBody.ext_outcome_submission_submitted_at_accepted)
=== 'true'
)
),
};
}
// Parse navigation launch
let launchAppTitle: string
if (!wasAssignmentLaunch) {
launchAppTitle = String(launchBody.resource_link_title);
}
// Add simpler role booleans
let isAdmin: boolean;
let isDesigner: boolean;
let isInstructor: boolean;
let isLearner: boolean;
let isNonCreditLearner: boolean;
let isCreditLearner: boolean;
let isTA: boolean;
let isTTM: boolean;
let notInCourse: boolean;
let extRoles: string[] = [];
if (launchBody.ext_roles && String(launchBody.ext_roles).length > 0) {
extRoles = String(launchBody.ext_roles).split(',');
isAdmin = !!extRoles.includes(
'urn:lti:instrole:ims/lis/Administrator'
);
isInstructor = !!extRoles.includes(
'urn:lti:role:ims/lis/Instructor'
);
isTA = !!extRoles.includes(
'urn:lti:role:ims/lis/TeachingAssistant'
);
isDesigner = !!extRoles.includes(
'urn:lti:role:ims/lis/ContentDeveloper'
);
isCreditLearner = !!extRoles.includes(
'urn:lti:role:ims/lis/Learner'
);
isNonCreditLearner = !!extRoles.includes(
'urn:lti:role:ims/lis/Learner/NonCreditLearner'
);
isLearner = (
isCreditLearner
|| isNonCreditLearner
);
isTTM = (
isInstructor
|| isTA
|| isDesigner
);
notInCourse = (
!isInstructor
&& !isTA
&& !isDesigner
&& !isCreditLearner
&& !isNonCreditLearner
&& !isLearner
);
}
// Don't allow TTMs to be learners
if (isTTM) {
isLearner = false;
isCreditLearner = false;
isNonCreditLearner = false;
}
// Get list of simple role names
const roles = String(launchBody.roles || '').split(',');
// Parse custom parameters
const customParams = new Map<string, string>();
Object.keys(launchBody).forEach((prop) => {
// Check if this is a custom param that wasn't sent by Canvas itself
if (
!prop.startsWith('custom_')
|| CANVAS_CUSTOM_PARAMS.indexOf(prop) >= 0
) {
// Not a custom parameter. Skip!
return;
}
// Rename prop without "custom_" prefix
const shorterPropName = prop.substring(7);
// Save custom parameter
customParams.set(shorterPropName, String(launchBody[prop]));
});
// Create the launchInfo object
const launchInfo: LaunchInfo = {
timestamp: (Number.parseInt(launchBody.oauth_timestamp) * 1000),
canvasHost: (
thisIsDevEnvironment
? 'localhost:8088'
: String(launchBody.custom_canvas_api_domain)
),
courseId: Number.parseInt(launchBody.custom_canvas_course_id),
sisCourseId: String(launchBody.lis_course_offering_sourcedid),
userId: Number.parseInt(launchBody.custom_canvas_user_id),
userLoginId: String(launchBody.custom_canvas_user_login_id),
userFirstName: String(launchBody.lis_person_name_given),
userLastName: String(launchBody.lis_person_name_family),
userFullName: String(launchBody.lis_person_name_full),
userEmail: String(launchBody.lis_person_contact_email_primary),
userImage: String(launchBody.user_image),
isAdmin,
isInstructor,
isTA,
isDesigner,
isCreditLearner,
isNonCreditLearner,
isLearner,
isTTM,
notInCourse,
extRoles,
customParams: Object.fromEntries(customParams),
contextId: String(launchBody.context_id),
contextLabel: String(launchBody.context_label),
enrollmentState: String(launchBody.custom_canvas_enrollment_state),
workflowState: String(launchBody.custom_canvas_workflow_state),
launchPresentationTarget: String(
launchBody.launch_presentation_document_target
),
iframeWidth: (
launchBody.launch_presentation_width
? Number.parseInt(launchBody.launch_presentation_width)
: undefined
),
iframeHeight: (
launchBody.launch_presentation_height
? Number.parseInt(launchBody.launch_presentation_height)
: undefined
),
locale: String(launchBody.launch_presentation_locale || 'en'),
returnURL: String(launchBody.launch_presentation_return_url),
roles,
canvasInstance: String(launchBody.tool_consumer_instance_name),
resourceLinkId: String(launchBody.resource_link_id),
originalLTILaunchBody: launchBody,
launchType,
assignment,
outcome,
launchAppTitle,
consumerKey: String(launchBody.oauth_consumer_key),
};
/*----------------------------------------*/
/* Update State */
/*----------------------------------------*/
// Remove tokens if user just launched as someone else
if (
req.session
&& req.session.launchInfo
&& req.session.launchInfo.userId
&& (req.session.launchInfo.userId !== launchInfo.userId)
) {
// NOTE: Keep these values up-to-date with all the session variables added
// by caccl-authorizer
delete req.session.authInfo;
}
// Add launchInfo to the session
req.session.launchInfo = launchInfo;
// Save session
return new Promise((resolve, reject) => {
req.session.save((err) => {
if (err) {
return reject(err);
}
return resolve(undefined);
});
});
};
export default parseLaunch;