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 NestJ sử dụng framework Express hoặc Fastify để làm HTTP Server. Mặc dù 2 framework trên đã 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:

image.png

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:
image.png

  • 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 chỉ cần chỉnh sửa lại url trỏ về route lti trong Developer Key như hình dưới:
image.png
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

4. Sử dụng CLI tạo module LTI trong Nest

  • cd src && nest g res lti
  • Chọn vì ở đây chúng ta develop theo REST API nên các bạn chọn REST API.
  • 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:
image.png

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.

  • .env
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:

  • app.module.ts

...
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 request đến lti route, ở đây sẽ là nơi chúng ta thêm vào logic của ltijs. Để 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
  • lti.middleware.ts
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);
 }
}
  • Thêm Middleware vào LTI Module
  • lti.module.ts
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');
  }
}

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 của Nest nên option sẽ là serverless

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.

image.png

image.png

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.
image.png
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.

image.png

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

Nguồn: viblo.asia

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *