Skip to content
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
99 changes: 99 additions & 0 deletions examples/courseContext.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Example: end-to-end per-tab course context with dce-expresskit.
*
* This file is illustrative teaching material — it is intentionally kept out of
* /src so it is not compiled into the published library. It shows the full loop:
*
* 1. (Server) Mint a course-context token for the current launch.
* 2. (Client) Store the token per browser tab and send it on every request.
* 3. (Server) genRouteHandler verifies the token automatically.
*
* Prerequisite: set DCEKIT_COURSE_CONTEXT_SECRET in the server environment. This
* secret signs and verifies tokens, so it must be stable and kept private.
*
* @author Karen Dolan
*/

/* eslint-disable @typescript-eslint/no-unused-vars */

// Import express
import express from 'express';

// Import expresskit
import {
addCourseContextEndpoint,
genRouteHandler,
COURSE_CONTEXT_HEADER,
} from 'dce-expresskit';

/*------------------------------------------------------------------------*/
/* ------------------------- 1. Server: mint it ------------------------- */
/*------------------------------------------------------------------------*/

const app = express();

// Option A (recommended): use the ready-made endpoint helper. This mounts
// GET /api/course-context, which returns a signed token for the current launch.
addCourseContextEndpoint({ app });

// Option B: mint inside your own endpoint if you need custom behavior.
// import { genCourseContext } from 'dce-expresskit';
// app.get('/api/my-course-context', genRouteHandler({
// handler: async ({ req }) => {
// return genCourseContext({ req });
// },
// }));

// Every normal endpoint stays exactly as it is today. When a verified course
// context is present on the request, genRouteHandler trusts it for this
// request's course + roles; when it is absent, behavior is unchanged.
app.get(
'/api/assignments',
genRouteHandler({
handler: async (
handlerOpts: {
params: { [k: string]: any },
},
) => {
// params.courseId / params.isTTM / params.isAdmin now reflect the verified
// course context for THIS tab, not just the shared session launch.
const {
courseId,
isTTM,
} = handlerOpts.params;
return { courseId, isTTM };
},
}),
);

/*------------------------------------------------------------------------*/
/* ------------------- 2. Client: store + attach it --------------------- */
/*------------------------------------------------------------------------*/

/**
* The snippet below is pseudocode for the client (for example, in a dce-reactkit
* app). Once dce-reactkit ships built-in support (Phase 2), visitServerEndpoint
* will do this automatically and app code will not need any of it. Until then,
* this shows the contract a client must honor.
*
* IMPORTANT: use sessionStorage, NOT localStorage. sessionStorage is scoped to a
* single browser tab, which is exactly what lets two tabs hold two different
* course contexts at once. localStorage is shared across tabs and would defeat
* the purpose.
*
* // On app launch, fetch and store the token for THIS tab:
* const { token } = await visitServerEndpoint({
* path: '/api/course-context',
* method: 'GET',
* });
* window.sessionStorage.setItem('courseContextToken', token);
*
* // On every request, attach the stored token as a header:
* const token = window.sessionStorage.getItem('courseContextToken');
* const headers = token ? { [COURSE_CONTEXT_HEADER]: token } : {};
*
* // If a request fails with the "expired" code (DEK39), re-mint once and retry:
* // 1) GET /api/course-context again, 2) store the new token, 3) retry.
*/

export default app;
15 changes: 15 additions & 0 deletions lib/constants/COURSE_CONTEXT_HEADER.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Name of the HTTP header that carries a signed course-context token.
*
* The client (for example, dce-reactkit's visitServerEndpoint) sends the token
* in this header, and genRouteHandler reads it to verify the per-request course
* context. Kept in a shared constant so the client and server never drift apart
* on the exact header name.
*
* Note: Express lower-cases incoming header names, so read it as
* req.headers['x-course-context'].
*
* @author Karen Dolan
*/
declare const COURSE_CONTEXT_HEADER = "X-Course-Context";
export default COURSE_CONTEXT_HEADER;
18 changes: 18 additions & 0 deletions lib/constants/COURSE_CONTEXT_HEADER.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/constants/COURSE_CONTEXT_HEADER.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions lib/helpers/addCourseContextEndpoint.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import express from 'express';
import COURSE_CONTEXT_HEADER from '../constants/COURSE_CONTEXT_HEADER';
/**
* Add an endpoint that mints a signed course-context token for the caller's
* current, Canvas-verified launch.
*
* This is the bootstrap for per-tab course contexts: the client calls this
* endpoint once right after a launch, stores the returned token per browser tab,
* and then sends it back on every subsequent request in the COURSE_CONTEXT_HEADER
* header (genRouteHandler verifies it). Because the token is derived from the
* session launch, this endpoint runs behind the normal session check — a user
* can only mint a context for a course they actually launched.
*
* The endpoint responds with the token as a plain string (wrapped in the
* standard success envelope), so the client can read it directly.
*
* @author Karen Dolan
* @param opts object containing all arguments
* @param opts.app the express app to add the endpoint to
* @param [opts.path=/api/course-context] the path to mount the endpoint at.
* Keep it under /api (all authorized users) — never /api/ttm or /api/admin,
* because every launched user legitimately needs to mint their own context.
* @param [opts.ttlMs] how long each minted token should remain valid, in ms
* (defaults to the library default inside genCourseContext)
* @example
* // Server: wire the endpoint once while setting up routes
* import express from 'express';
* import { addCourseContextEndpoint } from 'dce-expresskit';
*
* const app = express();
* addCourseContextEndpoint({ app });
* // → GET /api/course-context now returns a signed token for the launch
*/
declare const addCourseContextEndpoint: (opts: {
app: express.Application;
path?: string;
ttlMs?: number;
}) => void;
export { COURSE_CONTEXT_HEADER };
export default addCourseContextEndpoint;
102 changes: 102 additions & 0 deletions lib/helpers/addCourseContextEndpoint.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/helpers/addCourseContextEndpoint.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions lib/helpers/courseContext.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import VerifiedCourseAuth from '../types/VerifiedCourseAuth';
/**
* Mint a signed course-context token for the caller's current, Canvas-verified
* launch. The consumer app calls this (typically from a small endpoint) right
* after a launch and hands the token to the client, which stores it per browser
* tab and sends it back on each request (see COURSE_CONTEXT_HEADER).
* @author Karen Dolan
* @param opts object containing all arguments
* @param opts.req the express request (must have a valid CACCL launch)
* @param [opts.ttlMs=1 hour] how long the token should remain valid, in ms
* @returns a signed course-context token string
*/
declare const genCourseContext: (opts: {
req: any;
ttlMs?: number;
}) => string;
/**
* Verify a signed course-context token and return the course + roles it proves.
* Throws an ErrorWithCode if the token is malformed, has a bad signature, has
* expired, or was minted for a different user than the current session user.
*
* genRouteHandler calls this internally, so most consumers never need to. It is
* exported for apps that build their own middleware.
* @author Karen Dolan
* @param opts object containing all arguments
* @param opts.token the raw token string from the request header
* @param [opts.expectedUserId] if provided, the token's user must match this
* (bind the token to the logged-in session user to prevent replay by others)
* @returns the verified course authorization
*/
declare const verifyCourseContextToken: (opts: {
token: string;
expectedUserId?: number;
}) => VerifiedCourseAuth;
export { genCourseContext, verifyCourseContextToken, };
Loading