UNPKG

caccl-canvas-partial-simulator

Version:

Partially simulates a Canvas instance, handling OAuth token exchanges and forwarding API requests

407 lines (353 loc) 10.4 kB
/* eslint-disable no-console */ import express from 'express'; import clear from 'clear'; // Import caccl libs import initAPIForwarding from 'caccl-api-forwarder'; import initAPI from 'caccl-api'; import CanvasUserProfile from 'caccl-api/lib/types/CanvasUserProfile'; import serve from 'caccl-dev-server'; // Import shared types import User from './shared/types/User'; import Group from './shared/types/Group'; import API from 'caccl-api/lib/types/API'; // Import shared constants import CACCL_SIM_TOOL_ID from './shared/constants/CACCL_SIM_TOOL_ID'; // Import helpers import initLaunches from './initLaunches'; import initOAuth from './initOAuth'; import parallelLimit from './helpers/parallelLimit'; import printInstructionsAndExit from './printInstructionsAndExit'; import currentUser from './currentUser'; /*----------------------------------------*/ /* Helpers */ /*----------------------------------------*/ // Printing helpers const W = process.stdout.columns; /** * Calculate the number of spaces on the left of a centered line * @author Gabe Abrams * @param message the centered message * @returns number of spaces on the left */ const leftBuffer = (message: string) => { return (Math.floor(W / 2) - 1 - Math.ceil(message.length / 2)); }; /** * Calculate the number of spaces on the right of a centered line * @author Gabe Abrams * @param message the centered message * @returns number of spaces on the right */ const rightBuffer = (message: string) => { return (Math.ceil(W / 2) - 1 - Math.floor(message.length / 2)); }; /** * Center and surround text with a border (on left and right) * @author Gabe Abrams * @param str text to print */ const printMiddleLine = (str: string) => { console.log( '\u2551' + ' '.repeat(leftBuffer(str)) + str + ' '.repeat(rightBuffer(str)) + '\u2551' ); }; /** * Center text * @author Gabe Abrams * @param str text to print */ const printCenteredLine = (str: string) => { console.log( ' ' + ' '.repeat(leftBuffer(str)) + str + ' '.repeat(rightBuffer(str)) + ' ' ); }; /** * Print the top of a box * @author Gabe Abrams */ const printBoxTop = () => { // Print top of box console.log('\u2554' + '\u2550'.repeat(W - 2) + '\u2557'); }; /** * Print the bottom of a box * @author Gabe Abrams */ const printBoxBottom = () => { console.log('\u255A' + '\u2550'.repeat(W - 2) + '\u255D'); }; /** * Print an alert * @author Gabe Abrams * @param text message */ const printAlert = (text: string) => { clear(); printBoxTop(); printMiddleLine(text); printBoxBottom(); console.log(''); }; /*----------------------------------------*/ /* Set Up */ /*----------------------------------------*/ // Get the current working directory const workingDir = ( (process && process.env) ? (process.env.INIT_CWD || process.env.PWD) : '.' ); // Get dev environment let devEnvironment: { [k: string]: any }; try { // Read the file devEnvironment = require(`${workingDir}/config/devEnvironment.json`); if (!devEnvironment) { throw new Error(); } } catch (err) { printAlert('Dev Environment Not Found!'); printInstructionsAndExit(); } // Make sure required fields are included if (!devEnvironment.teacherAccessToken) { printAlert('Dev Environment Has No "teacherAccessToken"'); printInstructionsAndExit(); } if (!devEnvironment.courseId) { printAlert('Dev Environment Has No "courseId"'); printInstructionsAndExit(); } // Get app package.json let packageJSON: { [k: string]: any }; try { // Read the file packageJSON = require(`${workingDir}/package.json`); } catch (err) { printAlert('Missing package.json File'); console.log('Make sure you\'re starting the app from the top-level directory, which must also be an npm project.'); process.exit(0); } if (!packageJSON.name) { printAlert('Invalid package.json File'); console.log('Make sure your package.json includes a "name" parameter.'); process.exit(0); } /** * Initialize a simulated Canvas environment that automatically responds to * OAuth authorization requests and forwards all other requests * @author Gabe Abrams */ const start = async () => { /* ------------- Config ------------- */ // Set up default values and make sure required values exist const canvasHost = String( devEnvironment.canvasHost || 'canvas.instructure.com' ); const appName = packageJSON.name; const courseId: number = devEnvironment.courseId; const teacherAccessToken: string = devEnvironment.teacherAccessToken; const taAccessTokens: string[] = (devEnvironment.taAccessTokens ?? []); const studentAccessTokens: string[] = (devEnvironment.studentAccessTokens ?? []); const customParams: { [k: string]: any } = (devEnvironment.customParams); const customLaunchPaths: { name: string, path: string, }[] = (devEnvironment.customLaunchPaths ?? []); /* ------------- Server ------------- */ // Set up Express const app = express(); // Set up ejs app.set('view engine', 'ejs'); // Set up body parsing app.use(express.json()); app.use(express.urlencoded({ extended: true, limit: '5mb' })); // Allow cross origin connections app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ); res.setHeader('Access-Control-Request-Headers', '*'); next(); }); /* -------- Initialize Users -------- */ // Show a loader clear(); printCenteredLine('Waiting on Canvas...'); // Create a main API object const teacherAPI = initAPI({ canvasHost, accessToken: teacherAccessToken, }); let teacherProfile: CanvasUserProfile; // Verify that this API is an teacher and pull their profile try { // Pull Canvas info const [ studentObjects, profile, ] = await Promise.all([ teacherAPI.course.listStudents({ courseId }), teacherAPI.user.self.getProfile(), ]); // Save profile and id to teacher teacherProfile = profile; const teacherId = profile.id; for (let i = 0; i < studentObjects.length; i++) { if (studentObjects[i].id === teacherId) { // The "teacher" is a student! console.log('\nOops! The main access token in your /config/devEnvironment.js belongs to a student in the course. The main access token must belong to an teacher in the course.'); process.exit(0); } } } catch (err) { console.log(`\nOops! An error occurred while attempting to verify the teacher API: ${err.message}`); process.exit(0); } const teacher: User = { id: teacherProfile.id, group: Group.Teacher, index: 0, token: teacherAccessToken, api: teacherAPI, profile: teacherProfile, }; // Create API objects for each student const studentAPIs: API[] = studentAccessTokens.map((studentAccessToken) => { return initAPI({ canvasHost, accessToken: studentAccessToken, }); }); // Get profiles for each student let studentProfiles: CanvasUserProfile[]; try { studentProfiles = await parallelLimit( studentAPIs.map((api) => { return async () => { return api.user.self.getProfile(); }; }), 10, ); } catch (err) { console.log(`\nOops! An error occurred while attempting to get info on a test student: ${err.message}`); process.exit(0); } const students: User[] = studentAPIs.map((api, i) => { return { id: studentProfiles[i].id, group: Group.Student, index: i, token: studentAccessTokens[i], api, profile: studentProfiles[i], }; }); // Create API objects for each TA const taAPIs: API[] = taAccessTokens.map((taAccessToken) => { return initAPI({ canvasHost, accessToken: taAccessToken, }); }); // Get profiles for each ta let taProfiles: CanvasUserProfile[]; try { taProfiles = await parallelLimit( taAPIs.map((api) => { return async () => { return api.user.self.getProfile(); }; }), 10, ); } catch (err) { console.log(`\nOops! An error occurred while attempting to get info on a test ta: ${err.message}`); process.exit(0); } const tas: User[] = taAPIs.map((api, i) => { return { id: taProfiles[i].id, group: Group.TA, index: i, token: taAccessTokens[i], api, profile: taProfiles[i], }; }); /* --------------------- Initialize Services -------------------- */ // Initialize LTI launches await initLaunches({ app, appName, courseId, teacher, tas, students, customParams, customLaunchPaths, }); // Initialize OAuth initOAuth(app); // Add data to current user manager currentUser.addData(teacher, tas, students); /* ----------------- Initialize Canvas Redirects ---------------- */ // Self launch intercept app.get( `/courses/:courseId/external_tools/${CACCL_SIM_TOOL_ID}`, async (req, res) => { // Show the launch chooser in self-launch form return res.redirect('/simulator?isSelfLaunch=true'); }, ); // Redirect GET requests that aren't to the API app.get( '*', (req, res, next) => { // Skip if this is an API call if (req.path.startsWith('/api')) { return next(); } // Redirect to Canvas return res.redirect(`https://${canvasHost}${req.originalUrl}`); }, ); // Initialize the API initAPIForwarding({ app, numRetries: 1, forwarderPrefix: '', defaultCanvasHost: canvasHost, }); /* ------------------------ Start Server ------------------------ */ // Start HTTPS server await serve({ app, port: 8088, }); /* --------------------- Print Start Message -------------------- */ // Print alert clear(); printBoxTop(); printMiddleLine('Semi-simulated Canvas Now Running'); printBoxBottom(); console.log(''); console.log('To launch your app, visit:'); console.log('https://localhost:8088/simulator'); // Self-signed message console.log('\nYou may need to accept our self-signed certificate'); }; export default start;