이전 모듈에서는 Snyk 앱 등록, 인증 흐름 설정, 앱 내 사용자 인증 처리에 대해 다루었습니다. 이러한 주제들은 모든 Snyk 앱의 기능에 필수적이지만, 소위 "백그라운드" 작업에 해당합니다.
이 모듈에서는 Snyk 앱을 승인한 사용자에게 콘텐츠를 표시하는 것에 집중해 보겠습니다. 구체적으로, 인증되지 않은 사용자에게는 인증을 위한 커다란 버튼을 보여주고, 인증된 사용자에게는 Snyk 프로젝트 목록을 보여주고자 합니다.
Snyk 앱에 템플릿 엔진 추가
Express는 화면에 콘텐츠를 출력하고 서버 측에서 HTML을 렌더링하는 기능이 충분하지만, 템플릿 엔진을 사용하면 작업이 훨씬 수월해집니다. 이 튜토리얼에서는 EJS를 사용합니다.
먼저, 튜토리얼의 이 부분에 필요한 노드 패키지를 설치합니다.
npminstall--saveejs
다음으로, 이전 모듈에서 만든 initGlobalMiddlewares() 함수를 수정하여 Express에게 _뷰 엔진(view engine)_으로 EJS를 사용할 것임을 알리고, 뷰 템플릿을 저장할 위치를 지정합니다. EJS 템플릿은 ./src/views에 저장하고, 이미지나 CSS와 같은 공통 파일은 ./src/public에 보관하겠습니다.
템플릿을 제공할 각 라우트에 대해 해당 컨트롤러를 수정하고, 단순한 res.send() 대신 res.render("<템플릿 이름>")을 사용해야 합니다.
예시:
이게 전부입니다.
EJS 템플릿은 부분 포함(Partial inclusion) 개념을 지원합니다. 반드시 필요한 것은 아니지만, ./src/views에 하위 디렉토리를 추가하여 헤더나 푸터와 같은 부분 템플릿을 라우트 템플릿과 구분하는 것이 좋습니다. 튜토리얼에서는 이러한 템플릿을 저장하기 위해 ./src/views/partials를 사용하겠습니다.
기본 EJS 템플릿
가장 먼저 만들 템플릿은 다른 템플릿에 포함될 부분 템플릿입니다. 이 header.ejs는 스타일시트 및 HTML 문서의 <head>에 속하는 기타 정보들을 링크하는 공간이 될 것입니다.
이 index.ejs 템플릿은 기본적인 / 라우트를 담당합니다.
callback.ejs는 사용자 인증이 성공했을 때 렌더링됩니다.
이 템플릿들은 여러분이 새로 만드는 라우트에 자신만의 템플릿을 추가하기 시작하는 데 충분한 기반이 될 것입니다. EJS를 계속 사용할 계획이라면 제공되는 기능들에 대한 정보를 문서를 통해 확인하십시오.
Snyk 앱의 콘텐츠를 렌더링하는 것은 원하는 만큼 간단하게 또는 복잡하게 구성할 수 있습니다. JavaScript를 다루고 있기 때문에 옵션은 매우 유연합니다!
사용자에게 프로젝트 목록 표시
기본적인 템플릿이 준비되었으므로, 사용자의 Snyk 데이터를 사용하여 Snyk 앱에 기능을 추가하는 방법을 살펴보겠습니다. 이 튜토리얼에서는 사용자가 앱 내에서 자신의 모든 Snyk 프로젝트를 볼 수 있도록 앱을 구성하겠습니다.
이는 기본적인 기능이며 쉽게 확장할 수 있습니다.
다음을 생성해야 합니다.
새로운 라우트 컨트롤러
프로젝트 데이터를 가져오기 위한 함수(들)
프로젝트를 보여주기 위한 EJS 템플릿
먼저 이전 모듈에서 만든 callSnykApi() 함수를 사용하여 API 작업을 시작합니다. 이 작업은 특정 라우트와 직접적으로 관련이 있으므로, 이 파일을 해당 컨트롤러와 함께 저장하겠습니다. 이 튜토리얼 모듈 전체에서 사용한 패턴에 따라 두 파일 모두 ./src/routes/projects/에 생성하겠습니다.
다음으로 라우트 컨트롤러를 작성합니다. 패턴을 따릅니다: ./src/routes/projects/projectsController.ts.
새로운 라우트 컨트롤러를 추가할 때마다 ./index.ts를 업데이트하여 포함시켜야 합니다.
마무리 (Wrap-up)
이 모듈에서 만든 프로젝트의 API 핸들러와 컨트롤러를 사용하면, 나만의 맞춤형 코드를 작성하고 Snyk 앱이 원하는 대로 작동하도록 만드는 데 필요한 모든 것을 갖추게 된 것입니다.
여기서는 v1 API를 사용했지만 Snyk의 REST API도 주시하십시오. 추가 기능이 추가됨에 따라 Snyk 앱에서 사용할 수 있는 더 새롭고 효율적인 엔드포인트를 찾을 수 있을 것입니다.
// ./src/routes/projects/projectsHandler.ts
import { readFromDb } from "../../util/DB";
import { callSnykApi } from "../../util/apiHelpers";
import { EncryptDecrypt } from "../../util/encrypt-decrypt";
import { AuthData } from "../../interfaces/DB";
import { APIVersion } from "../../interfaces/API";
import { ENCRYPTION_SECRET } from "../../app";
/**
* 사용자 액세스 토큰을 사용하여 Snyk API에서 모든 사용자 프로젝트를
* 가져오는 프로젝트 핸들러입니다. 이는 예시 목적으로 작성되었습니다.
* 프로덕션 환경에서는 토큰 범위에 따라 액세스 가능한 항목이
* 달라질 수 있습니다.
* @returns 사용자 프로젝트 목록 또는 빈 배열
*/
export async function getProjectsFromApi(): Promise<unknown[]> {
// DB에서 데이터 읽기
const db = await readFromDb();
const data = mostRecent(db.installs);
// 데이터가 없으면 빈 배열 반환
if (!data) return [];
// 데이터(액세스 토큰) 복호화
const eD = new EncryptDecrypt(ENCRYPTION_SECRET as string);
const access_token = eD.decryptString(data?.access_token);
const token_type = data?.token_type;
// Snyk API v1으로 구성된 axios 인스턴스 호출
const result = await callSnykApi(
token_type,
access_token,
APIVersion.V1
).post(`/org/${data?.orgId}/projects`);
return result.data.projects || [];
}
/**
*
* @param {AuthData[]} installs 설치 목록에서 가장 최근의 설치 정보를 가져옴
* @returns 최신 설치 정보 또는 void
*/
export function mostRecent(installs: AuthData[]): AuthData | void {
if (installs) {
return installs[installs.length - 1];
}
return;
}
// ./src/routes/projects/projectsController.ts
import type { Controller } from "../../interfaces/Controller";
import type { NextFunction, Request, Response } from "express";
import { Router } from "express";
import { getProjectsFromApi } from "./projectsHandler";
export class ProjectsController implements Controller {
// 이 컨트롤러의 기본 URL 경로
public path = "/projects";
// 이 컨트롤러의 Express 라우터
public router = Router();
constructor() {
this.initRoutes();
}
private initRoutes() {
// 모든 사용자 프로젝트 목록을 렌더링하기 위한 라우트
this.router.get(`${this.path}`, this.getProjects);
}
private async getProjects(req: Request, res: Response, next: NextFunction) {
try {
const projects = await getProjectsFromApi();
return res.render("projects", {
projects,
});
} catch (error) {
return next(error);
}
}
}
// ./src/index.ts
import IndexController from "./routes/index/indexController";
import AuthController from "./routes/auth/authController";
import CallbackController from "./routes/callback/callbackController";
import ProjectsController from "./routes/projects/projectsController";
import App from "./app";
new App([
new IndexController(),
new AuthController(),
new CallbackController(),
new ProjectsController()],
3000
);