ltijs
Version:
Easily turn your web application into a LTI 1.3 Learning Tool.
179 lines • 10.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Provider = void 0;
const default_logger_service_1 = require("#services/logger/default/default-logger.service");
const fetch_request_handler_service_1 = require("#services/request-handler/fetch/fetch-request-handler.service");
const mongo_database_manager_service_1 = require("#services/database-manager/mongo/mongo-database-manager.service");
const express_http_handler_service_1 = require("#services/http-handler/express/express-http-handler.service");
const mock_cache_manager_service_1 = require("#services/cache-manager/mock/mock-cache-manager.service");
const platform_manager_service_1 = require("#services/platform-manager/platform-manager.service");
const access_token_manager_service_1 = require("#services/access-token-manager/access-token-manager.service");
const oidc_service_1 = require("#services/oidc/oidc.service");
const launch_service_1 = require("#services/launch/launch.service");
const keyset_service_1 = require("#services/keyset/keyset.service");
const dynamic_registration_service_1 = require("#services/dynamic-registration/dynamic-registration.service");
const errors_1 = require("#services/provider/errors");
/**
* The library's main entry point. Construct one per application (`new Provider(options)`), wire up
* launch handlers, then call {@link Provider.listen} to start listening. Everything else, platform
* registration, resuming a launch outside the original request, closing down cleanly, goes through
* this instance or the sub-services exposed on it.
*/
class Provider {
DEFAULT_LOGIN_ROUTE = '/lti/login';
DEFAULT_LAUNCH_ROUTE = '/lti/launch';
DEFAULT_KEYSET_ROUTE = '/lti/keys';
DEFAULT_DYNAMIC_REGISTRATION_ROUTE = '/lti/register';
DEFAULT_PORT = 3000;
DEFAULT_TOKEN_MAX_AGE_SECONDS = 10;
STARTUP_BANNER = ' _ _______ _____ _ _____\n' +
' | | |__ __|_ _| | |/ ____|\n' +
' | | | | | | | | (___ \n' +
' | | | | | | _ | |\\___ \\ \n' +
' | |____| | _| |_| |__| |____) |\n' +
' |______|_| |_____|\\____/|_____/ \n';
/** The active storage backend. Defaults to `MongoDatabaseManager`; override via `ProviderOptions.databaseManager`. */
databaseManager;
/** Register, look up, update, and (de)activate platforms. See its own methods for the full CRUD surface. */
platformManager;
/** The active HTTP framework adapter. Defaults to `ExpressHttpHandler`; override via `ProviderOptions.httpHandler`. */
httpHandler;
/** The active cache backend. Defaults to a no-op; override via `ProviderOptions.cacheManager` (e.g. `RedisCacheManager`). */
cacheManager;
/** Only set when `ProviderOptions.dynamicRegistration` was provided at construction. */
dynamicRegistrationService;
logger;
loginRoute;
launchRoute;
keysetRoute;
dynamicRegistrationRoute;
launchService;
port;
/**
* Every dependency (`databaseManager`, `cacheManager`, `httpHandler`, `requestHandler`, `logger`) has a
* working default: pass nothing beyond a database connection (or a full `databaseManager`) to get a
* runnable provider. Route defaults are `/lti/login`, `/lti/launch`, `/lti/keys`, `/lti/register`.
*/
constructor(options) {
this.logger = options.logger ?? new default_logger_service_1.DefaultLogger();
const requestHandler = options.requestHandler ?? new fetch_request_handler_service_1.FetchRequestHandler();
this.cacheManager = options.cacheManager ?? new mock_cache_manager_service_1.MockCacheManager();
this.databaseManager =
options.databaseManager ?? new mongo_database_manager_service_1.MongoDatabaseManager(this.logger, options.database);
const serverOptions = { port: this.DEFAULT_PORT, ...options.server };
this.httpHandler = options.httpHandler ?? new express_http_handler_service_1.ExpressHttpHandler(this.logger, serverOptions);
this.port = serverOptions.port;
this.platformManager = new platform_manager_service_1.PlatformManager(this.databaseManager, this.logger, this.cacheManager);
const accessTokenManager = new access_token_manager_service_1.AccessTokenManager(this.databaseManager, requestHandler, this.logger);
const oidcService = new oidc_service_1.OidcService(this.databaseManager, requestHandler, this.cacheManager, this.logger, options.tokenMaxAge ?? this.DEFAULT_TOKEN_MAX_AGE_SECONDS);
this.loginRoute = options.routes?.loginRoute ?? this.DEFAULT_LOGIN_ROUTE;
this.launchRoute = options.routes?.launchRoute ?? this.DEFAULT_LAUNCH_ROUTE;
this.keysetRoute = options.routes?.keysetRoute ?? this.DEFAULT_KEYSET_ROUTE;
this.launchService = new launch_service_1.LaunchService(oidcService, this.platformManager, accessTokenManager, this.databaseManager, requestHandler, this.httpHandler, this.logger);
this.launchService.prepareHttpRoutes({ loginRoute: this.loginRoute, launchRoute: this.launchRoute });
const keysetService = new keyset_service_1.KeysetService(this.platformManager, this.httpHandler, this.cacheManager, this.logger);
keysetService.prepareHttpRoutes(this.keysetRoute);
const dynamicRegistration = this.setupDynamicRegistration(options, requestHandler);
this.dynamicRegistrationRoute = dynamicRegistration?.route;
this.dynamicRegistrationService = dynamicRegistration?.service;
}
/** Called on a standard resource-link launch (the common case: a student/instructor opening the tool from the LMS). */
onResourceLink(handler) {
this.launchService.setOnResourceLinkHandler(handler);
}
/** Alias of `onResourceLink()`, matching legacy's exact method name for anyone migrating from legacy ltijs. */
onConnect(handler) {
this.onResourceLink(handler);
}
/** Called when the platform launches the tool for content selection (the deep-linking message type). */
onDeepLinking(handler) {
this.launchService.setOnDeepLinkingHandler(handler);
}
/** Called on a submission-review launch (an instructor reviewing a learner's graded submission). */
onSubmissionReview(handler) {
this.launchService.setOnSubmissionReviewHandler(handler);
}
/**
* Called instead of throwing the default `UnregisteredPlatformError` when a login request arrives
* from a platform this tool hasn't registered yet. Must fully send the response itself.
*/
onUnregisteredPlatform(handler) {
this.launchService.setOnUnregisteredPlatformHandler(handler);
}
/**
* Called instead of throwing the default `PlatformNotActivatedError` when a login request arrives
* from a platform that's registered but deactivated. Must fully send the response itself.
*/
onInactivePlatform(handler) {
this.launchService.setOnInactivePlatformHandler(handler);
}
/**
* Overrides the default GET handler for the dynamic-registration route. Throws
* `DynamicRegistrationNotConfiguredError` if `ProviderOptions.dynamicRegistration` wasn't set.
*/
onDynamicRegistration(handler) {
if (this.dynamicRegistrationService === undefined)
throw new errors_1.DynamicRegistrationNotConfiguredError();
this.dynamicRegistrationService.setHandler(handler);
}
/**
* Resumes a previously-issued launch by its ltik: the session-resumption entry point for a
* follow-up request (e.g. protecting a custom app route, or submitting a grade from a background
* job) using a ltik obtained from an earlier `LaunchContext`.
*/
async getLaunchContext(ltik) {
return await this.launchService.getLaunchContext(ltik);
}
/** Registers `path` as an additional launch route, dispatching through the same launch handlers. */
registerLtiRoute(path) {
this.launchService.registerLaunchRoute(path);
}
setupDynamicRegistration(options, requestHandler) {
if (options.dynamicRegistration === undefined)
return undefined;
const route = options.routes?.dynamicRegistrationRoute ?? this.DEFAULT_DYNAMIC_REGISTRATION_ROUTE;
const service = new dynamic_registration_service_1.DynamicRegistration(options.dynamicRegistration, { appRoute: this.launchRoute, loginRoute: this.loginRoute, keysetRoute: this.keysetRoute }, this.platformManager, requestHandler, this.httpHandler, this.logger);
service.prepareHttpRoutes(route);
return { route, service };
}
/**
* Connects the database and cache backends, starts the HTTP listener, and registers a `SIGINT` handler
* that calls {@link Provider.close} before exiting. Prints a startup banner unless `options.silent`.
* Port and TLS were already given to the `httpHandler` at construction time (see
* `ProviderOptions.server`, which only applies to the default `ExpressHttpHandler`; a custom
* `httpHandler` is expected to already be fully configured).
*/
async listen(options = {}) {
await this.databaseManager.listen();
await this.cacheManager.listen();
await this.httpHandler.listen();
if (options.silent !== true)
this.printStartupBanner(this.port);
process.on('SIGINT', () => {
void this.close().finally(() => process.exit());
});
}
/** @deprecated Alias of {@link Provider.listen}, kept for backwards compatibility. */
async deploy(options = {}) {
await this.listen(options);
}
/** Stops the HTTP listener and closes the database and cache connections, in that order. */
async close() {
await this.httpHandler.close();
await this.databaseManager.close();
await this.cacheManager.close();
}
printStartupBanner(port) {
const message = `LTI Provider is listening on port ${port}!\n\n` +
' LTI provider config: \n' +
` >Login Route: ${this.loginRoute}\n` +
` >Launch Route: ${this.launchRoute}\n` +
` >Keyset Route: ${this.keysetRoute}\n` +
` >Dynamic Registration Route: ${this.dynamicRegistrationRoute ?? '(disabled)'}`;
console.log(this.STARTUP_BANNER);
console.log(message);
}
}
exports.Provider = Provider;
exports.default = Provider;
//# sourceMappingURL=provider.service.js.map