Cài đặt LTI cho Canvas LMS với NestJS Framework
Xin chào các bạn, Tết của các bạn sao ùi. Hôm nay mùng 2 mình giành tí thời gian để viết tiếp Series, ở Phần 1 chúng ta đã tìm hiểu về LTI cũng như cài đặt LTI trên NodeJS thuần, hôm nay chúng ta sẽ cùng đến với Phần 2 của Series Sử dụng LTI với Canvas LMS. Ở phần này chúng ta sẽ cùng cài đặt ltijs trên NestJS Framework để tận dụng các chức năng cần thiết của framework này.
Mình sẽ nói qua sơ lược về NestJS Framework để các bạn chưa sử dụng có thể làm quen, các bạn nào đã sử dụng qua thì có thể kéo xuống trực tiếp phần cài đặt để tiết kiệm thời gian nhé.
NestJS là gì?
A progressive Node.js framework for building efficient, reliable and scalable server-side applications.
NestJS (Nest) là một framework dùng để phát triển ứng dụng NodeJS hiệu quả và có khả năng mở rộng cao. Về cơ bản NestJS sử dụng framework Express hoặc Fastify để làm HTTP Server. Mặc dù 2 framework trên đa phần đều giải quyết được đa số nhu cầu của chúng ta trong việc xây dựng và phát triển ứng dụng nhưng nó vẫn chưa đảm bảo được các đặc tính clean structure, highly scalable, testable và dễ dàng maintaince.
Nest kết hợp cả 3 yếu tố OOP(Object Oriented Programming), FP(Functional Programming), FRP(Functional Reactive Programming). Nest supports Typescript, mình khá là thích tính năng này, vì Nest giúp chúng ta cài đặt Typescript tự động. Tuy nhiên nếu các bạn chưa làm quen với Typescript thì Nest vẫn hỗ trợ các bạn viết bằng Javascript thuần.
Tại sao mình sử dụng NestJS cho LTI?
Có 1 số lý do có thể kể đến như:
- Cho phép develop nhanh và hiệu quả: Nest cung cấp Nest CLI giúp generate code tự động giúp mình tiết kiệm được thời gian mỗi khi tạo thêm các logic mới. Các bạn có thể tham khảo hình dưới:
Chỉ cần gõ lệnh theo cú pháp của CLI thì sẽ tự tạo ra các file mặc định cho chúng ta.
Ví dụ khi gõ nest g res lti
thì Nest CLI sẽ tạo ra các file cho chúng ta như hình:
- Hỗ trợ Typescript: như mình đã nói ở trên, Nest tự động cấu hình Typescript compiler nên chúng ta không cần tốn thời gian cài đặt thủ công - lúc mới học Typescript mình đã tốn kha khá thời gian cho quá trình config Typescript compiler.
- Nest sử dụng Dependency Injection (DI): giúp tự động ủy quyền phụ thuộc các module cho inversion of control (IoC) thay vì chúng ta phải ủy quyền thủ công.
Cài đặt LTI trên Canvas
Ở Phần 1 chúng ta đã cài đặt xong Developer Key trên Canvas, ở phần này chúng ta sẽ cho LTI chạy ở route /lti
nên sẽ chỉnh sửa lại url trỏ về http://localhost:3333/lti
trong Developer Key như hình dưới:
Nếu bạn nào chưa tạo thì có thể quay lại Phần 1 để xem chi tiết các bước.
Cài đặt LTI trên NestJS Framework
Chúng ta sẽ tiến hành cài đặt package ltijs, thời điểm hiện tại của bài viết mình đang dùng ubuntu:24.04, nestjs/*:v9.0.0 và package ltijs:v5.9.0.
1. Tạo thư mục và khởi tạo project nestjs:
nest new lti-account-role
Sau đó chọn npm , yarn hoặc pnpm tùy theo cách dùng của các bạn, mình sẽ chọn npm.
2. Install các package của NestJS
npm install
3. Cài đặt package ltijs và các package cần thiết:
npm install ltijs @nestjs/config joi
- @nestjs/config: cấu hình đọc file chứa biến env
- joi: validate biến env
4. Sử dụng CLI tạo module LTI trong Nest
cd src && nest g res lti
- Chọn
Y
vì ở đây chúng ta develop theo REST API nên các bạn chọn yes. - Do phạm vi bài viết của chúng ta chỉ cần kết nối ltijs nên chúng ta không cần tạo CRUD entry points nên các bạn chọn
n
.
Chúng ta sẽ được cấu trúc như hình dưới:
5. Cấu hình biến môi trường:
Tạo file .env với nội dung tương tự như phần trước.
DATABASE_NAME=lti_acocunt_role
DATABASE_USERNAME=admin
DATABASE_PASSWORD=admin
DATABASE_PORT=27022
DATABASE_URI=mongodb://localhost:27022
PORT=3333 // PORT để Canvas kết nối vào LTI
LTI_HOST=https://your-canvas-domain.com // Các bạn đổi thành domain Canvas các bạn đang dùng nhé
LTI_CLIENT_ID=10000000000022 // Đây là Client ID từ Developer key chúng ta tạo khi nảy
LTI_KEY=HkvZwP0DKtqWTUjX1qNjQdWiSBCZmGWNe7iRR73ke9MiosdVSrY583urVouN8mk5 // tương tự đây là Secret key từ Developer key
LTI_NAME=LTI_ACCOUNT_ROLE
LTI_ISS=https://canvas.instructure.com // ISS phải trùng với ISS trong file security.yml trong config của Canvas
-
Nest cung cấp package @nestjs/config giúp cấu hình biến môi trường - về bản chất thì package này sử dụng package dotenv như chúng ta đã dùng ở Phần 1 nhưng khi sử dụng sẽ dễ dàng thao tác với Dependency Injection.
-
Package joi giúp chúng ta kiểm tra xem các biến môi trường đã khai báo đầy đủ và đúng định dạng hay chưa. Chúng ta sẽ chỉnh sửa lại file app.module.ts như sau:
...
import * as Joi from 'joi';
@Module({
imports: [
LtiModule,
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_NAME: Joi.string().required(),
DATABASE_USERNAME: Joi.string().required(),
DATABASE_PASSWORD: Joi.string().required(),
DATABASE_PORT: Joi.number().required(),
DATABASE_URI: Joi.string().required(),
PORT: Joi.number().required(),
LTI_KEY: Joi.string().required(),
LTI_HOST: Joi.string().required(),
LTI_CLIENT_ID: Joi.string().required(),
LTI_NAME: Joi.string().required(),
LTI_ISS: Joi.string().required(),
}),
isGlobal: true, // cho phép sử dụng Config Service ở mọi nơi
}),
],
...
6. Tạo file LTI middleware
- Middleware này sẽ được gọi trước khi các request đến LTI server, ở đây sẽ là nơi chúng ta thêm vào logic của ltijs để xác thực các request gọi đến, mục đích để đảm bảo chỉ có các request xác thực từ Canvas LMS hoặc request gọi API từ giao diện của chính LTI là có thể thông qua. Để sử dụng Middleware trong Nest chúng ta implement interface NestMiddleware. Sau đó dùng interface OnModuleInit để cấu hình ltijs cũng như kết nối ltijs đến Database. Vì chúng ta đã cấu hình Config Service nên có thể inject để sử dụng.
2 interfaces trên được cung cấp sẵn trong @nestjs/common
import { Injectable, NestMiddleware, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response } from 'express';
import { Provider as lti } from 'ltijs';
@Injectable()
export class LtiMiddleware implements NestMiddleware, OnModuleInit {
/**
*
*/
constructor(private readonly config_service: ConfigService) {}
async onModuleInit() { // Các config ltijs ở đây sẽ tương tự Phần 1
lti.setup(
this.config_service.get<string>('LTI_KEY')!, // Cách lấy ra biến môi trường
{
url:
this.config_service.get<string>('DATABASE_URI') +
'/' +
this.config_service.get<string>('DATABASE_NAME') +
'?authSource=admin',
connection: {
user: this.config_service.get<string>('DATABASE_USERNAME'),
pass:
this.config_service.get<string>('DATABASE_PASSWORD'),
},
},
{
appRoute: '/',
invalidTokenRoute: '/invalidtoken',
sessionTimeoutRoute: '/sessionTimeout',
keysetRoute: '/keys',
loginRoute: '/login',
devMode: true,
tokenMaxAge: 60,
},
);
// Whitelisting the main app route and /nolti to create a landing page
lti.whitelist(
{
route: new RegExp(/^\/nolti$/),
method: 'get',
},
{
route: new RegExp(/^\/ping$/),
method: 'get',
}
);
lti.onConnect((token, req: Request, res: Response) => {
if (token) {
res.json(res.locals?.context?.custom?.role);
} else res.redirect('/lti/nolti');
});
await lti.deploy({ serverless: true });
await lti.registerPlatform({
url: this.config_service.get<string>('LTI_ISS'),
name: this.config_service.get<string>('LTI_NAME'),
clientId: this.config_service.get<string>('LTI_CLIENT_ID'),
authenticationEndpoint: `${this.config_service.get<string>(
'LTI_HOST',
)}/api/lti/authorize_redirect`,
accesstokenEndpoint: `${this.config_service.get<string>(
'LTI_HOST',
)}/login/oauth2/token`,
authConfig: {
method: 'JWK_SET',
key: `${this.config_service.get<string>(
'LTI_HOST',
)}/api/lti/security/jwks`,
},
});
}
// Request sẽ đến đây trước khi vào controller lti
// Chúng ta sẽ cho kết nối với ltijs ở đây
use(req: Request, res: Response, next: () => void) {
lti.app(req, res, next);
}
}
Lưu ý: các bạn để ý dòng
await lti.deploy({ serverless: true });
sẽ khác với Phần 1, ở Phần 1 chúng ta dùng Express Server của ltijs nên sẽ để option port, còn ở đây chúng ta sẽ sử dụng Server đang chạy Nest nên option sẽ là serverless
- Thêm Middleware vào LTI Module
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { LtiController } from './lti.controller';
import { LtiMiddleware } from './lti.middleware';
@Module({
controllers: [LtiController],
})
export class LtiModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LtiMiddleware).forRoutes('lti');
}
}
7. Tạo controller để test kết nối
import { Controller, Get, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Controller('lti')
export class LtiController {
@Get('nolti')
async nolti(@Req() req: Request, @Res() res: Response) {
res.send(
'There was a problem getting you authenticated with the attendance application. Please contact support.',
);
}
@Get('ping')
async ping(@Req() req: Request, @Res() res: Response) {
res.send('pong');
}
@Get('protected')
async protected(@Req() req: Request, @Res() res: Response) {
res.send('Insecure');
}
}
8. Chạy MongoDB:
Chúng ta vẫn cần chạy MongoDB để ltijs lưu các cấu hình kết nối.
9. Chạy Server bằng lệnh:
npm run start:dev
- Nếu phản hồi như hình dưới thì server đã khởi chạy thành công.
- Truy cập http://localhost:3333/lti/ping để kiểm tra. Nếu message trả về là "pong" thì đã set up lti thành công.
- Truy cập http://localhost:3333 chúng ta sẽ trả về route mặc định được tạo bởi Nest CLI
Chúng ta có thể thấy nếu truy cập vào các route bên trong LTI mà không có trong whitelist
sẽ trả về message NO_LTIK_OR_IDTOKEN_FOUND, còn nếu ở ngoài lti thì vẫn có thể truy cập như bình thường.
Vậy là chúng ta đã cấu hình xong ltijs trong Nest, giờ chúng ta sẽ quay lại Canvas LMS để kiểm tra xem đã kết nối thành công chưa. Chúng ta có thể thấy kết quả trả về tương tự phần 1.
Kết luận
Vậy là chúng ta đã tạo xong 1 custom LTI với NestJS Framework, chúng ta có thể tận dụng các công nghệ mà Nest mang lại để phát triển dự án một cách thuận tiện và tối ưu. Phần cuối của series mình sẽ là tích hợp FE chạy bằng ReactJS vào LTI Server của phần này để khởi chạy 1 LTI hoàn chỉnh.
Cảm ơn các bạn đã quan tâm theo dõi. Nếu có câu hỏi gì các bạn hãy comment phía dưới hoặc inbox riêng cho mình. Chúc các bạn mùng 3 Tết vui vẻ và thành công.
Các bạn có thể tải về source code của phần tại đây.
Tài liệu tham khảo
All rights reserved