diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b1473da --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +PORT=3000 + +# MySQL connection used by Prisma +DATABASE_URL=mysql://umc_app:change-me@localhost:3306/umc_10th +DB_HOST=localhost +DB_PORT=3306 +DB_USER=umc_app +DB_PASSWORD=change-me +DB_NAME=umc_10th + +# Google OAuth +PASSPORT_GOOGLE_CLIENT_ID=replace-with-google-client-id +PASSPORT_GOOGLE_CLIENT_SECRET=replace-with-google-client-secret +PASSPORT_GOOGLE_CALLBACK_URL=http://localhost:3000/oauth2/callback/google + +# JWT signing secret +JWT_SECRET=replace-with-a-long-random-secret diff --git "a/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" "b/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" new file mode 100644 index 0000000..9c9bb7c --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/-00\354\260\250-\354\260\254\354\260\254-\354\233\214\355\201\254\353\266\201-\354\240\234\354\266\234\355\225\251\353\213\210\353\213\244.md" @@ -0,0 +1,10 @@ +--- +name: "[00차/찬찬]워크북 제출합니다" +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git "a/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" "b/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" index 991b3c2..1d2c061 100644 --- "a/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" +++ "b/.github/ISSUE_TEMPLATE/\354\233\214\355\201\254\353\266\201-\354\235\264\354\212\210.md" @@ -2,8 +2,9 @@ name: 워크북 이슈 템플릿 about: 해당 이슈 생성 템플릿을 사용하여 워크북 이슈를 생성합니다. title: '"Chapter00 워크북 제목"' -labels: "" -assignees: seoki +labels: '' +assignees: '' + --- ### 📚 워크북 diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml new file mode 100644 index 0000000..d727871 --- /dev/null +++ b/.github/workflows/deploy-main.yml @@ -0,0 +1,157 @@ +name: deploy-main + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deploy-main + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Create build environment + shell: bash + env: + EC2_DOT_ENV: ${{ secrets.EC2_DOT_ENV }} + run: printf '%s' "$EC2_DOT_ENV" > .env + + - name: Generate and build + run: npm run build + + - name: Keep production dependencies only + run: npm prune --omit=dev + + - name: Pack artifact + shell: bash + run: | + mkdir -p artifact + cp -r dist artifact/dist + cp -r node_modules artifact/node_modules + cp -r prisma artifact/prisma + cp package.json package-lock.json prisma.config.ts artifact/ + tar -czf artifact.tgz -C artifact . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: app-artifact + path: artifact.tgz + if-no-files-found: error + retention-days: 1 + + deploy: + runs-on: ubuntu-latest + needs: build + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: app-artifact + + - name: Extract artifact + run: | + mkdir -p artifact + tar -xzf artifact.tgz -C artifact + + - name: Configure SSH + shell: bash + env: + EC2_HOST: ${{ secrets.EC2_HOST }} + EC2_PORT: ${{ secrets.EC2_PORT }} + EC2_USER: ${{ secrets.EC2_USER }} + EC2_SSH_KEY: ${{ secrets.EC2_SSH_KEY }} + run: | + install -m 700 -d ~/.ssh + printf '%s\n' "$EC2_SSH_KEY" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -p "$EC2_PORT" -H "$EC2_HOST" >> ~/.ssh/known_hosts + chmod 600 ~/.ssh/known_hosts + cat >> ~/.ssh/config < /opt/app/umc10th/incoming/.env && chmod 600 /opt/app/umc10th/incoming/.env' + + - name: Apply database migrations + shell: bash + run: | + ssh target "cd /opt/app/umc10th/incoming && source /home/ubuntu/.nvm/nvm.sh && npm run migrate:deploy" + + - name: Activate release + shell: bash + run: | + ssh target ' + set -eu + cd /opt/app/umc10th + rm -rf previous + if [ -d current ]; then mv current previous; fi + mv incoming current + ' + + - name: Configure and restart systemd service + shell: bash + run: | + cat <<'EOF' | ssh target 'sudo tee /etc/systemd/system/umc10th.service > /dev/null' + [Unit] + Description=UMC 10th Node.js API + After=network-online.target mysql.service + Wants=network-online.target + + [Service] + Type=simple + User=ubuntu + WorkingDirectory=/opt/app/umc10th/current + ExecStart=/bin/bash -lc 'source /home/ubuntu/.nvm/nvm.sh && exec npm start' + Restart=always + RestartSec=3 + Environment=NODE_ENV=production + + [Install] + WantedBy=multi-user.target + EOF + + ssh target 'sudo systemctl daemon-reload && sudo systemctl enable umc10th && sudo systemctl restart umc10th' + + - name: Verify deployment + shell: bash + run: | + ssh target 'sudo systemctl is-active --quiet umc10th' + ssh target 'curl --fail --silent --show-error --retry 5 --retry-delay 2 http://127.0.0.1:3000/' + ssh target 'sudo systemctl status umc10th --no-pager --lines=30' diff --git a/.gitignore b/.gitignore index 4d64059..efac5ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /node_modules - +.env +.env.* +!.env.example +dist/ +src/generated/prisma/ diff --git a/misson_practice/chapter07/README.md b/misson_practice/chapter07/README.md new file mode 100644 index 0000000..13af5b3 --- /dev/null +++ b/misson_practice/chapter07/README.md @@ -0,0 +1,1343 @@ + +# 7주차 실습 README + +이번 7주차 실습에서는 기존 Express 프로젝트에 Tsoa를 적용하고, Express 미들웨어와 API 응답 통일, 에러 핸들링 구조를 적용했다. + +6주차까지는 Prisma ORM을 이용해 API 기능 구현에 집중했다면, 7주차에서는 API 구조를 더 일관성 있게 만들기 위해 다음 내용을 순차적으로 진행했다. + +- Tsoa 적용 +- Express 미들웨어 적용 +- morgan, cookie-parser 적용 +- 성공 응답 형식 통일 +- 실패 응답 형식 통일 +- 커스텀 Error 객체 적용 +- 전역 에러 핸들러 적용 +- Postman으로 성공/실패 응답 테스트 + +--- + +## 0. GitHub 작업 정보 + +### Issue + +```text +[Practice] 7주차 Tsoa, 미들웨어, 응답 통일 실습 +```` + +### Branch + +```text +feature/chapter-07 +``` + +### 작업 방식 + +이번 7주차 실습은 실습 코드와 README 정리가 함께 포함되므로 하나의 브랜치에서 진행했다. + +커밋은 기능별로 분리했다. + +```text +feat: 7주차 Tsoa 및 미들웨어 적용 +docs: 7주차 실습 README 정리 +docs: 7주차 미션 README 정리 +``` + +### 브랜치 이동 + +```bash +cd C:\Project\10th_Node.js_Practice_Mission + +git fetch origin +git switch feature/chapter-07 +``` + +만약 브랜치가 로컬에 없다면 다음 명령어를 사용한다. + +```bash +git switch -c feature/chapter-07 origin/feature/chapter-07 +``` + +현재 브랜치 확인: + +```bash +git branch +``` + +정상이라면 아래처럼 표시된다. + +```text +* feature/chapter-07 +``` + +--- + +## 1. 실습 전 현재 상태 확인 + +먼저 현재 프로젝트 상태를 확인했다. + +```bash +git status +``` + +올라가면 안 되는 파일이 있는지 확인한다. + +```text +.env +node_modules/ +dist/ +``` + +위 파일들은 GitHub에 올라가면 안 되므로 `.gitignore`에 포함되어 있어야 한다. + +`.gitignore` 확인: + +```gitignore +node_modules/ +.env +.env.* +dist/ +src/generated/prisma/ +``` + +--- + +## 2. Tsoa 설치 + +### 2-1. 설치 명령어 + +```bash +npm install express tsoa +``` + +설치 중 다음과 같은 경고가 나올 수 있다. + +```text +npm warn deprecated glob@10.5.0 +3 vulnerabilities +``` + +### 2-2. 로그 해석 + +이 경고는 설치 실패가 아니라, 하위 의존성 중 일부 패키지에서 deprecated 또는 보안 취약점 경고가 발생했다는 의미이다. + +설치 자체는 아래처럼 완료되면 성공이다. + +```text +added ... packages +audited ... packages +``` + +### 2-3. 설치 확인 + +```bash +npm list tsoa +npm list express +``` + +또는 Tsoa 버전 확인: + +```bash +npx tsoa --version +``` + +### 실습 인증 사진 + + +![Tsoa 설치 확인](./images/practice-01-tsoa-install.png) + +--- + +## 3. tsconfig.json 수정 + +Tsoa는 데코레이터 문법을 사용하므로 `tsconfig.json`에 아래 옵션을 추가한다. + +```json +{ + "compilerOptions": { + "experimentalDecorators": true + } +} +``` + +이미 `compilerOptions`가 있다면 그 안에 `"experimentalDecorators": true`만 추가하면 된다. + +### 예시 + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "outDir": "./dist" + } +} +``` + +### 왜 필요한가? + +`@Route`, `@Post`, `@Get`, `@Tags` 같은 Tsoa 데코레이터를 사용하기 위해 필요하다. + +### 실습 인증 사진 + + +![tsconfig decorator 설정](./images/practice-02-tsconfig.png) + +--- + +## 4. tsoa.json 생성 + +프로젝트 루트에 `tsoa.json` 파일을 생성한다. + +```text +10th_Node.js_Practice_Mission +└── tsoa.json +``` + +내용은 다음과 같이 작성한다. + +```json +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*.controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3 + }, + "routes": { + "routesDir": "src/generated" + } +} +``` + +### 설정 설명 + +| 설정 | 의미 | +| -------------------------------- | --------------------------- | +| `entryFile` | Express 서버의 진입 파일 | +| `controllerPathGlobs` | Tsoa가 controller 파일을 찾는 경로 | +| `noImplicitAdditionalProperties` | DTO에 없는 추가 필드가 들어왔을 때 에러 처리 | +| `spec.outputDirectory` | OpenAPI 문서 생성 위치 | +| `routes.routesDir` | 자동 생성되는 routes.ts 위치 | + +### 실습 인증 사진 + + +![tsoa.json 생성](./images/practice-03-tsoa-json.png) + +--- + +## 5. 공통 응답 폴더 생성 + +성공 응답 구조를 통일하기 위해 공통 응답 파일을 만든다. + +### 폴더 생성 + +```bash +mkdir -p src/common/responses +``` + +PowerShell에서는 다음처럼 해도 된다. + +```powershell +mkdir src\common\responses +``` + +### 파일 생성 + +```text +src/common/responses/response.ts +``` + +### response.ts 작성 + +```ts +export interface ApiResponse { + resultType: "SUCCESS"; + error: null; + success: T; +} + +export const success = (data: T): ApiResponse => ({ + resultType: "SUCCESS", + error: null, + success: data, +}); +``` + +### 코드 설명 + +```ts +resultType: "SUCCESS" +``` + +요청이 성공했음을 나타낸다. + +```ts +error: null +``` + +성공 응답이므로 에러는 없다. + +```ts +success: data +``` + +실제 응답 데이터는 `success` 안에 담는다. + +--- + +## 6. 공통 Error 폴더 생성 + +실패 응답을 통일하기 위해 커스텀 Error 클래스를 만든다. + +### 폴더 생성 + +```bash +mkdir -p src/common/errors +``` + +PowerShell: + +```powershell +mkdir src\common\errors +``` + +--- + +## 7. AppError 생성 + +### 파일 생성 + +```text +src/common/errors/app.error.ts +``` + +### 코드 작성 + +```ts +export class AppError extends Error { + public readonly errorCode: string; + public readonly statusCode: number; + public readonly data?: unknown; + + constructor(params: { + errorCode: string; + message: string; + statusCode: number; + data?: unknown; + }) { + super(params.message); + + this.errorCode = params.errorCode; + this.statusCode = params.statusCode; + this.data = params.data ?? null; + } +} +``` + +### 코드 설명 + +기본 `Error` 객체는 주로 `message`만 가진다. +하지만 API 응답에서는 다음 정보가 필요하다. + +```text +errorCode +statusCode +message +data +``` + +그래서 `AppError`를 만들어 공통 에러 구조를 정의했다. + +--- + +## 8. 사용자 관련 커스텀 Error 생성 + +### 파일 생성 + +```text +src/common/errors/user.error.ts +``` + +### 코드 작성 + +```ts +import { AppError } from "./app.error.js"; + +export class DuplicateUserEmailError extends AppError { + constructor(data?: unknown) { + super({ + errorCode: "U001", + message: "이미 존재하는 이메일입니다.", + statusCode: 409, + data, + }); + } +} +``` + +### 코드 설명 + +```ts +errorCode: "U001" +``` + +이메일 중복 오류를 구분하기 위한 코드이다. + +```ts +statusCode: 409 +``` + +이미 존재하는 이메일은 기존 리소스 상태와 충돌하는 상황이므로 `409 Conflict`로 처리했다. + +--- + +## 9. User DTO 정리 + +Tsoa에서는 Controller에서 요청/응답 타입을 직접 사용하므로 DTO를 인터페이스 중심으로 정리한다. + +### 파일 위치 + +```text +src/modules/users/dtos/user.dto.ts +``` + +### 코드 예시 + +```ts +export interface UserSignUpRequest { + email: string; + password?: string; + name: string; + gender: string; + birth: Date | string; + address?: string; + detailAddress?: string; + phoneNumber: string; + preferences: number[]; +} + +export interface UserSignUpResponse { + userId: number; + preferences: string[]; +} +``` + +### 정리 + +기존에 `bodyToUser()` 같은 변환 함수가 있었다면, Tsoa 방식에서는 Controller에서 `@Body()`로 직접 타입을 받기 때문에 DTO는 요청과 응답의 타입 정의 중심으로 정리한다. + +--- + +## 10. User Service 수정 + +이메일 중복 오류가 발생할 때 일반 Error가 아니라 커스텀 Error를 던지도록 수정한다. + +### 파일 위치 + +```text +src/modules/users/services/user.service.ts +``` + +### 코드 예시 + +```ts +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { + addUser, + getUser, + getUserPreferencesByUserId, + setPreference, +} from "../repositories/user.repository.js"; +import { DuplicateUserEmailError } from "../../../common/errors/user.error.js"; + +export const userSignUp = async ( + data: UserSignUpRequest +): Promise => { + const joinUserId = await addUser({ + email: data.email, + password: data.password, + name: data.name, + gender: data.gender, + birth: new Date(data.birth), + address: data.address ?? "", + detailAddress: data.detailAddress ?? "", + phoneNumber: data.phoneNumber, + }); + + if (joinUserId === null) { + throw new DuplicateUserEmailError(data); + } + + for (const preference of data.preferences) { + await setPreference(joinUserId, preference); + } + + const user = await getUser(joinUserId); + + const preferences = (await getUserPreferencesByUserId(joinUserId)).map( + (obj) => obj.foodCategory.name + ); + + return { + userId: user.id, + preferences, + }; +}; +``` + +### 변경 전 + +```ts +throw new Error("이미 존재하는 이메일입니다."); +``` + +### 변경 후 + +```ts +throw new DuplicateUserEmailError(data); +``` + +--- + +## 11. User Controller를 Tsoa 방식으로 변경 + +기존 Express 함수형 컨트롤러를 Tsoa 클래스 기반 컨트롤러로 변경한다. + +### 파일 위치 + +```text +src/modules/users/controllers/user.controller.ts +``` + +### 기존 방식 + +```ts +export const handleUserSignUp = async (req, res, next) => { + const user = await userSignUp(req.body); + + res.status(200).json({ + result: user, + }); +}; +``` + +### Tsoa 방식 + +```ts +import { Body, Controller, Post, Route, Tags } from "tsoa"; +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { userSignUp } from "../services/user.service.js"; +import { ApiResponse, success } from "../../../common/responses/response.js"; + +@Route("users") +@Tags("Users") +export class UserController extends Controller { + @Post("signup") + public async handleUserSignUp( + @Body() body: UserSignUpRequest + ): Promise> { + console.log("회원가입을 요청했습니다!"); + console.log("body:", body); + + const user = await userSignUp(body); + + return success(user); + } +} +``` + +### 코드 설명 + +```ts +@Route("users") +``` + +이 컨트롤러의 기본 경로를 의미한다. +최종적으로 `/api/v1/users`로 연결된다. + +```ts +@Post("signup") +``` + +`POST /users/signup` 엔드포인트를 의미한다. + +```ts +@Body() body: UserSignUpRequest +``` + +요청 body를 `UserSignUpRequest` 타입으로 받는다. + +```ts +return success(user); +``` + +성공 응답을 공통 응답 형식으로 감싸서 반환한다. + +--- + +## 12. Tsoa routes 생성 + +컨트롤러를 Tsoa 방식으로 수정한 뒤 routes 파일을 생성한다. + +```bash +npx tsoa spec-and-routes +``` + +정상적으로 생성되면 아래 파일이 생긴다. + +```text +src/generated/routes.ts +``` + +### 확인 + +```bash +ls src/generated +``` + +PowerShell: + +```powershell +dir src\generated +``` + +### 실습 인증 사진 + +```md +![Tsoa routes 생성](./images/practice-04-generated-routes.png) +``` + +--- + +## 13. index.ts에 Tsoa routes 연결 + +### 파일 위치 + +```text +src/index.ts +``` + +기존에는 `index.ts`에서 직접 라우트를 등록했다. + +```ts +app.post("/api/v1/users/signup", handleUserSignUp); +``` + +이제 Tsoa가 생성한 routes를 연결한다. + +### index.ts 예시 + +```ts +import dotenv from "dotenv"; +import express, { Express, NextFunction, Request, Response } from "express"; +import cors from "cors"; +import { RegisterRoutes } from "./generated/routes.js"; +import { AppError } from "./common/errors/app.error.js"; + +dotenv.config(); + +const app: Express = express(); +const port = process.env.PORT || 3000; + +app.use(cors()); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +const router = express.Router(); + +RegisterRoutes(router); + +app.use("/api/v1", router); + +app.use((err: AppError, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + return next(err); + } + + res.status(err.statusCode || 500).json({ + resultType: "FAIL", + error: { + errorCode: err.errorCode || "UNKNOWN", + reason: err.message || "서버 오류가 발생했습니다.", + data: err.data || null, + }, + success: null, + }); +}); + +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); +}); +``` + +### 중요한 순서 + +```ts +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +RegisterRoutes(router); +app.use("/api/v1", router); + +app.use((err, req, res, next) => { + ... +}); +``` + +전역 에러 핸들러는 routes 등록 이후에 위치해야 한다. + +--- + +## 14. package.json scripts 수정 + +서버 실행 전 Tsoa routes를 자동 생성하도록 scripts를 수정한다. + +### package.json + +```json +{ + "scripts": { + "start": "tsoa spec-and-routes && tsx src/index.ts", + "dev": "tsoa spec-and-routes && nodemon --exec tsx src/index.ts" + } +} +``` + +### 의미 + +```text +tsoa spec-and-routes +→ src/generated/routes.ts 생성 +→ tsx src/index.ts 실행 +``` + +이렇게 하면 컨트롤러 수정 후 routes 생성을 깜빡하는 문제를 줄일 수 있다. + +--- + +## 15. 서버 실행 테스트 + +```bash +npm run dev +``` + +정상 실행 예시: + +```text +[server]: Server is running at http://localhost:3000 +``` + +### 실습 인증 사진 + +```md +![서버 실행 성공](./images/practice-05-server-run.png) +``` + +--- + +## 16. Postman으로 회원가입 성공 응답 테스트 + +### Request + +```text +POST http://localhost:3000/api/v1/users/signup +``` + +### Body + +```json +{ + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] +} +``` + +### 예상 Response + +```json +{ + "resultType": "SUCCESS", + "error": null, + "success": { + "userId": 2, + "preferences": ["한식", "중식"] + } +} +``` + +### 확인할 점 + +```text +resultType이 SUCCESS인지 확인 +error가 null인지 확인 +실제 데이터가 success 안에 들어가는지 확인 +``` + +### 실습 인증 사진 + + +![회원가입 성공 응답](./images/practice-06-signup-success.png) + + +--- + +## 17. Postman으로 중복 이메일 실패 응답 테스트 + +같은 이메일로 다시 요청을 보낸다. + +### Request + +```text +POST http://localhost:3000/api/v1/users/signup +``` + +### Body + +```json +{ + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] +} +``` + +### 예상 Response + +```json +{ + "resultType": "FAIL", + "error": { + "errorCode": "U001", + "reason": "이미 존재하는 이메일입니다.", + "data": { + "email": "new-user@example.com", + "password": "1234abcd!", + "name": "새유저", + "gender": "남성", + "birth": "2000-02-03", + "address": "주소1", + "detailAddress": "세부주소1", + "phoneNumber": "010-0000-0000", + "preferences": [1, 2] + } + }, + "success": null +} +``` + +### 확인할 점 + +```text +resultType이 FAIL인지 확인 +errorCode가 U001인지 확인 +reason이 이미 존재하는 이메일입니다.인지 확인 +success가 null인지 확인 +HTTP Status가 409인지 확인 +``` + +### 실습 인증 사진 + + +![중복 이메일 실패 응답](./images/practice-07-signup-fail.png) + + +--- + +## 18. morgan 설치 및 적용 + +### 설치 + +```bash +npm install morgan +npm install -D @types/morgan +``` + +### index.ts 적용 + +```ts +import morgan from "morgan"; + +app.use(morgan("dev")); +``` + +`cors()` 아래, 라우터 등록 전에 작성한다. + +```ts +app.use(cors()); +app.use(morgan("dev")); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +``` + +### 확인 + +Postman으로 아무 API나 요청하면 터미널에 로그가 출력된다. + +```text +POST /api/v1/users/signup 409 12.456 ms - 350 +``` + +--- + +## 19. cookie-parser 설치 및 적용 + +### 설치 + +```bash +npm install cookie-parser +npm install -D @types/cookie-parser +``` + +### index.ts 적용 + +```ts +import cookieParser from "cookie-parser"; + +app.use(cookieParser()); +``` + +적용 위치: + +```ts +app.use(cors()); +app.use(morgan("dev")); +app.use(cookieParser()); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +``` + +--- + +## 20. 인증 미들웨어 생성 + +### 폴더 생성 + +```bash +mkdir -p src/common/middlewares +``` + +PowerShell: + +```powershell +mkdir src\common\middlewares +``` + +### 파일 생성 + +```text +src/common/middlewares/auth.middleware.ts +``` + +### 코드 작성 + +```ts +import { Request, Response, NextFunction } from "express"; + +export function authorizeUser() { + return async (req: Request, res: Response, next: NextFunction) => { + const { username } = req.cookies; + + if (username) { + console.log(`[인증 성공] ${username}님, 환영합니다.`); + next(); + } else { + console.log("[인증 실패] 로그인이 필요합니다."); + + res + .status(401) + .send( + '' + ); + } + }; +} +``` + +### 코드 설명 + +```ts +const { username } = req.cookies; +``` + +`cookie-parser`를 통해 쿠키 값을 객체로 읽는다. + +```ts +next(); +``` + +로그인 쿠키가 있으면 다음 단계로 넘어간다. + +```ts +res.status(401) +``` + +쿠키가 없으면 인증 실패로 처리한다. + +--- + +## 21. Tsoa Controller에 인증 예제 추가 + +`src/modules/users/controllers/user.controller.ts`에 아래 import를 추가한다. + +```ts +import { + Body, + Controller, + Get, + Middlewares, + Post, + Request, + Route, + Tags, +} from "tsoa"; +import { Request as ExpressRequest } from "express"; +import { authorizeUser } from "../../../common/middlewares/auth.middleware.js"; +``` + +UserController 안에 아래 메서드들을 추가한다. + +```ts +@Get("guest") +public async handleGuestPage(): Promise { + return ` +

게스트 페이지

+

이 페이지는 로그인이 필요 없습니다.

+ + `; +} + +@Get("login") +public async handleLoginPage(): Promise { + return "

로그인 페이지

로그인이 필요한 페이지에서 이동합니다.

"; +} + +@Get("mypage") +@Middlewares(authorizeUser()) +public async handleMypage( + @Request() req: ExpressRequest +): Promise { + return ` +

마이페이지

+

환영합니다, ${req.cookies.username}님!

+

이 페이지는 로그인한 사람만 볼 수 있습니다.

+ `; +} + +@Get("set-login") +public async handleSetLogin( + @Request() req: ExpressRequest +): Promise { + req.res!.cookie("username", "UMC10th", { maxAge: 3600000 }); + + return '로그인 쿠키 생성 완료! 마이페이지로 이동'; +} + +@Get("set-logout") +public async handleSetLogout( + @Request() req: ExpressRequest +): Promise { + req.res!.clearCookie("username"); + + return '로그아웃 완료! 게스트 페이지로 이동'; +} +``` + +--- + +## 22. routes 재생성 및 서버 재실행 + +Controller에 새로운 endpoint를 추가했으므로 routes를 다시 생성해야 한다. + +```bash +npx tsoa spec-and-routes +``` + +또는 서버 실행 시 자동 생성되도록 했다면: + +```bash +npm run dev +``` + +--- + +## 23. 브라우저로 인증 미들웨어 테스트 + +브라우저에서 아래 순서대로 접속한다. + +### 1단계. 게스트 페이지 접속 + +```text +http://localhost:3000/api/v1/users/guest +``` + +### 2단계. 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +쿠키가 없으면 로그인 필요 alert가 뜨고 `/api/v1/users/login`으로 이동한다. + +### 3단계. 로그인 쿠키 생성 + +```text +http://localhost:3000/api/v1/users/set-login +``` + +### 4단계. 다시 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +이번에는 정상적으로 마이페이지가 열린다. + +### 5단계. 로그아웃 + +```text +http://localhost:3000/api/v1/users/set-logout +``` + +### 6단계. 다시 마이페이지 접속 + +```text +http://localhost:3000/api/v1/users/mypage +``` + +다시 로그인 필요 alert가 뜨면 성공이다. + +### 실습 인증 사진 + + +![게스트 페이지](./images/practice-09-guest-page.png) +![로그인 필요 화면](./images/practice-10-login-required.png) +![쿠키 생성](./images/practice-11-set-login.png) +![마이페이지 성공](./images/practice-12-mypage-success.png) +![로그아웃](./images/practice-13-logout.png) + + +--- + +## 24. 실습 완료 후 최종 확인 + +### 서버 실행 + +```bash +npm run dev +``` + +### 확인해야 할 API + +```text +POST /api/v1/users/signup +GET /api/v1/users/guest +GET /api/v1/users/login +GET /api/v1/users/mypage +GET /api/v1/users/set-login +GET /api/v1/users/set-logout +``` + +### 확인해야 할 내용 + +```text +[ ] Tsoa routes가 생성되는가? +[ ] 서버가 정상 실행되는가? +[ ] 회원가입 성공 응답이 SUCCESS 구조로 반환되는가? +[ ] 중복 이메일 실패 응답이 FAIL 구조로 반환되는가? +[ ] morgan 로그가 터미널에 찍히는가? +[ ] cookie-parser로 쿠키를 읽을 수 있는가? +[ ] 인증 미들웨어가 mypage 접근을 제어하는가? +``` + +--- + +## 25. 실습 중 발생한 이슈 정리 + +### 이슈 1. tsoa 설치 중 deprecated 및 vulnerabilities 경고 발생 + +#### 문제 + +```text +npm warn deprecated glob@10.5.0 +3 vulnerabilities +``` + +#### 원인 + +`tsoa` 설치 과정에서 함께 설치되는 하위 의존성 중 일부 패키지에서 deprecated 경고와 보안 취약점 경고가 발생했다. + +#### 해결 + +설치 자체는 정상 완료되었는지 먼저 확인했다. + +```bash +npm list tsoa +npm list express +``` + +이후 취약점 상세 내용은 아래 명령어로 확인했다. + +```bash +npm audit +``` + +`npm audit fix --force`는 major version 변경으로 프로젝트가 깨질 수 있으므로 바로 실행하지 않았다. + +--- + +### 이슈 2. generated/routes.ts를 찾을 수 없음 + +#### 문제 + +```text +Cannot find module './generated/routes' +``` + +#### 원인 + +Tsoa routes 파일을 아직 생성하지 않았다. + +#### 해결 + +```bash +npx tsoa spec-and-routes +``` + +또는 `package.json` scripts에 아래처럼 추가했다. + +```json +"dev": "tsoa spec-and-routes && nodemon --exec tsx src/index.ts" +``` + +--- + +### 이슈 3. 데코레이터 오류 발생 + +#### 문제 + +`@Route`, `@Post` 같은 데코레이터 사용 시 TypeScript 오류가 발생했다. + +#### 원인 + +`tsconfig.json`에 데코레이터 사용 설정이 없었다. + +#### 해결 + +```json +"experimentalDecorators": true +``` + +--- + +### 이슈 4. 실패 응답이 UNKNOWN으로만 내려옴 + +#### 문제 + +```json +{ + "resultType": "FAIL", + "error": { + "errorCode": "UNKNOWN", + "reason": "이미 존재하는 이메일입니다.", + "data": null + }, + "success": null +} +``` + +#### 원인 + +일반 `Error` 객체에는 `errorCode`, `statusCode`가 없기 때문이다. + +#### 해결 + +`AppError`를 상속한 커스텀 Error를 만들고 Service에서 해당 Error를 던지도록 수정했다. + +```ts +throw new DuplicateUserEmailError(data); +``` + +--- + +## 26. 실습 인증 체크리스트 + +아래 사진을 `practice/chapter07/images/`에 저장한다. + +```text +[ ] practice-01-tsoa-install.png +[ ] practice-02-tsconfig.png +[ ] practice-03-tsoa-json.png +[ ] practice-04-generated-routes.png +[ ] practice-05-server-run.png +[ ] practice-06-signup-success.png +[ ] practice-07-signup-fail.png +[ ] practice-08-morgan-log.png +[ ] practice-09-guest-page.png +[ ] practice-10-login-required.png +[ ] practice-11-set-login.png +[ ] practice-12-mypage-success.png +[ ] practice-13-logout.png +``` + +--- + +## 27. Git commit 정리 + +### 실습 코드 커밋 + +```bash +git add src package.json package-lock.json tsconfig.json tsoa.json +git commit -m "feat: 7주차 Tsoa 및 미들웨어 적용" +``` + +### 실습 README 커밋 + +```bash +git add practice/chapter07/README.md +git add practice/chapter07/images/ +git commit -m "docs: 7주차 실습 README 정리" +``` + +### Push + +```bash +git push -u origin feature/chapter-07 +``` + +--- + +## 28. 느낀 점 + +이번 실습을 통해 API는 단순히 기능이 동작하는 것뿐만 아니라, 응답 구조와 에러 처리 방식도 일관되게 관리해야 한다는 것을 느꼈다. + +Tsoa를 적용하면서 컨트롤러에서 API 경로와 요청 타입, 응답 타입을 더 명확하게 표현할 수 있었다. 또한 `success()` wrapper와 `AppError`를 적용하면서 성공 응답과 실패 응답의 구조를 통일할 수 있었다. + +`morgan`을 통해 요청 로그를 확인하고, `cookie-parser`와 인증 미들웨어를 통해 특정 라우트를 보호하는 흐름도 이해할 수 있었다. + +이번 실습을 통해 Express의 미들웨어 구조가 단순 보조 기능이 아니라 서버 요청 처리 흐름의 핵심이라는 점을 알게 되었다. + +```` diff --git a/misson_practice/chapter07/images/practice-01-tsoa-install.png b/misson_practice/chapter07/images/practice-01-tsoa-install.png new file mode 100644 index 0000000..ad63b58 Binary files /dev/null and b/misson_practice/chapter07/images/practice-01-tsoa-install.png differ diff --git a/misson_practice/chapter07/images/practice-02-tsconfig.png b/misson_practice/chapter07/images/practice-02-tsconfig.png new file mode 100644 index 0000000..aefb543 Binary files /dev/null and b/misson_practice/chapter07/images/practice-02-tsconfig.png differ diff --git a/misson_practice/chapter07/images/practice-03-tsoa-json.png b/misson_practice/chapter07/images/practice-03-tsoa-json.png new file mode 100644 index 0000000..3e8468f Binary files /dev/null and b/misson_practice/chapter07/images/practice-03-tsoa-json.png differ diff --git a/misson_practice/chapter07/images/practice-06-signup-success.png b/misson_practice/chapter07/images/practice-06-signup-success.png new file mode 100644 index 0000000..ccd4eeb Binary files /dev/null and b/misson_practice/chapter07/images/practice-06-signup-success.png differ diff --git a/misson_practice/chapter07/images/practice-07-signup-fail.png b/misson_practice/chapter07/images/practice-07-signup-fail.png new file mode 100644 index 0000000..71efb96 Binary files /dev/null and b/misson_practice/chapter07/images/practice-07-signup-fail.png differ diff --git a/misson_practice/chapter07/images/practice-09-guest-page.png b/misson_practice/chapter07/images/practice-09-guest-page.png new file mode 100644 index 0000000..eaec4db Binary files /dev/null and b/misson_practice/chapter07/images/practice-09-guest-page.png differ diff --git a/misson_practice/chapter07/images/practice-10-login-required.png b/misson_practice/chapter07/images/practice-10-login-required.png new file mode 100644 index 0000000..fa0897d Binary files /dev/null and b/misson_practice/chapter07/images/practice-10-login-required.png differ diff --git a/misson_practice/chapter07/images/practice-11-set-login.png b/misson_practice/chapter07/images/practice-11-set-login.png new file mode 100644 index 0000000..2af5cf8 Binary files /dev/null and b/misson_practice/chapter07/images/practice-11-set-login.png differ diff --git a/misson_practice/chapter07/images/practice-12-mypage-success.png b/misson_practice/chapter07/images/practice-12-mypage-success.png new file mode 100644 index 0000000..283481b Binary files /dev/null and b/misson_practice/chapter07/images/practice-12-mypage-success.png differ diff --git a/misson_practice/chapter07/images/practice-13-logout.png b/misson_practice/chapter07/images/practice-13-logout.png new file mode 100644 index 0000000..ff38876 Binary files /dev/null and b/misson_practice/chapter07/images/practice-13-logout.png differ diff --git a/misson_practice/chapter09/README.md b/misson_practice/chapter09/README.md new file mode 100644 index 0000000..f2cdfd0 --- /dev/null +++ b/misson_practice/chapter09/README.md @@ -0,0 +1,69 @@ +## 1. 실습 목표 + +9주차 실습의 목표는 Google OAuth 로그인을 구현하고, 로그인 성공 후 JWT를 발급받아 Bearer Token 방식으로 보호된 API에 접근하는 것이다. + +## 2. 설치한 패키지 + +```bash +npm install passport passport-google-oauth20 jsonwebtoken passport-jwt +npm install --save-dev @types/passport @types/passport-google-oauth20 @types/jsonwebtoken @types/passport-jwt +```` + +## 3. Google OAuth Client 설정 + +Google Cloud Console에서 OAuth Client ID를 발급받고, Redirect URI를 다음과 같이 설정하였다. + +```text +http://localhost:3000/oauth2/callback/google +``` + +![Google OAuth 설정](./images/01-google-oauth-client-setting.png) + +## 4. 환경변수 설정 + +`.env`에 Google Client ID, Client Secret, JWT Secret을 추가하였다. + +![환경변수 설정](./images/02-env-setting.png) + +## 5. Google 로그인 구현 + +`src/auth.config.ts`에서 Google Strategy와 JWT 생성 함수를 작성하였다. + +## 6. 로그인 라우트 추가 + +```ts +app.get( + "/oauth2/login/google", + passport.authenticate("google", { session: false }) +); +``` + +## 7. 로그인 성공 후 토큰 발급 확인 + +Google 로그인 성공 후 accessToken과 refreshToken이 응답되는 것을 확인하였다. + +![토큰 응답](./images/06-google-login-token-response.png) + +## 8. 보호 라우트 테스트 + +`/mypage` 라우트에 JWT 인증 미들웨어를 적용하였다. + +```ts +const isLogin = passport.authenticate("jwt", { session: false }); +``` + +Bearer Token을 넣고 요청했을 때는 성공하였다. + +![Bearer Token 성공](./images/07-mypage-with-bearer-token.png) + +토큰 없이 요청했을 때는 인증에 실패하였다. + +![토큰 없이 실패](./images/08-mypage-without-token.png) + +## 9. 느낀 점 + +이번 실습을 통해 Google OAuth 로그인은 외부 서비스에서 사용자를 인증받는 과정이고, 이후 우리 서버가 자체 JWT를 발급해야 우리 서비스의 API 인증에 사용할 수 있다는 점을 이해할 수 있었다. + +또한 JWT를 발급받는 것만으로는 인증이 끝나는 것이 아니라, API 요청 시 Authorization 헤더에 Bearer Token 형식으로 토큰을 담아 보내야 보호된 API를 사용할 수 있다는 점을 확인하였다. + +```` \ No newline at end of file diff --git a/misson_practice/chapter09/images/01-google-oauth-client-setting.png b/misson_practice/chapter09/images/01-google-oauth-client-setting.png new file mode 100644 index 0000000..014b5a9 Binary files /dev/null and b/misson_practice/chapter09/images/01-google-oauth-client-setting.png differ diff --git a/misson_practice/chapter09/images/02-env-setting.png b/misson_practice/chapter09/images/02-env-setting.png new file mode 100644 index 0000000..5fc450f Binary files /dev/null and b/misson_practice/chapter09/images/02-env-setting.png differ diff --git a/misson_practice/chapter09/images/03-server-running.png b/misson_practice/chapter09/images/03-server-running.png new file mode 100644 index 0000000..ce9e9b0 Binary files /dev/null and b/misson_practice/chapter09/images/03-server-running.png differ diff --git a/misson_practice/chapter09/images/04-google-login-page.png b/misson_practice/chapter09/images/04-google-login-page.png new file mode 100644 index 0000000..aaddd6f Binary files /dev/null and b/misson_practice/chapter09/images/04-google-login-page.png differ diff --git a/misson_practice/chapter09/images/05-google-consent-page.png b/misson_practice/chapter09/images/05-google-consent-page.png new file mode 100644 index 0000000..82d429c Binary files /dev/null and b/misson_practice/chapter09/images/05-google-consent-page.png differ diff --git a/misson_practice/chapter09/images/06-google-login-token-response.png b/misson_practice/chapter09/images/06-google-login-token-response.png new file mode 100644 index 0000000..1228173 Binary files /dev/null and b/misson_practice/chapter09/images/06-google-login-token-response.png differ diff --git a/misson_practice/chapter09/images/07-mypage-with-bearer-token.png b/misson_practice/chapter09/images/07-mypage-with-bearer-token.png new file mode 100644 index 0000000..3b44c11 Binary files /dev/null and b/misson_practice/chapter09/images/07-mypage-with-bearer-token.png differ diff --git a/misson_practice/chapter09/images/08-mypage-without-token.png b/misson_practice/chapter09/images/08-mypage-without-token.png new file mode 100644 index 0000000..a88d3c8 Binary files /dev/null and b/misson_practice/chapter09/images/08-mypage-without-token.png differ diff --git a/misson_practice/chapter10/README.md b/misson_practice/chapter10/README.md new file mode 100644 index 0000000..e6181d0 --- /dev/null +++ b/misson_practice/chapter10/README.md @@ -0,0 +1,367 @@ +# 10주차 실습: GitHub Actions와 AWS EC2로 CI/CD 구축하기 + +## 1. 실습 목표 + +- `main` 브랜치 변경을 GitHub Actions가 감지하도록 한다. +- GitHub Actions에서 애플리케이션을 빌드하고 배포 아티팩트를 만든다. +- SSH와 `rsync`를 이용해 아티팩트를 EC2로 전송한다. +- systemd로 Node.js 서버를 실행하고 재시작한다. +- 배포 환경에서도 Google OAuth 로그인이 동작하도록 콜백 URL을 분리한다. +- 구축한 파이프라인의 각 단계를 설명할 수 있도록 기록한다. + +## 2. 전체 배포 흐름 + +```text +main 브랜치 push + ↓ +GitHub Actions build job + - npm ci + - Prisma Client 및 TSOA 라우트 생성 + - TypeScript 컴파일 + - 운영 의존성만 남기기 + - artifact.tgz 생성 + ↓ +GitHub Actions deploy job + - SSH 설정 + - EC2로 아티팩트 전송 + - EC2에 .env 생성 + - current 디렉터리 교체 + - systemd 서비스 재시작 + - 서비스와 HTTP 응답 확인 +``` + +## 3. 로컬 프로젝트를 배포 구조로 변경 + +### 3-1. 실행 스크립트 분리 + +개발 환경에서는 `tsx`로 TypeScript를 바로 실행하지만, 배포 환경에서는 먼저 JavaScript로 컴파일한 후 `node`로 실행한다. + +```json +{ + "scripts": { + "build": "prisma generate --schema=./prisma/schema.prisma && tsoa spec-and-routes && tsc", + "start": "node dist/index.js", + "dev": "prisma generate --schema=./prisma/schema.prisma && tsoa spec-and-routes && nodemon --exec tsx src/index.ts" + } +} +``` + +모듈 형식은 TSOA가 생성하는 라우트와 맞도록 CommonJS로 통일했다. + +### 3-2. 빌드 확인 + +```bash +npm ci +npm run build +``` + +`dist/index.js`가 생성되고 명령이 오류 없이 끝나는지 확인한다. + +- [x] 의존성 잠금 파일 기준 설치 +- [x] Prisma Client 생성 +- [x] TSOA 라우트 생성 +- [x] TypeScript 컴파일 + +## 4. Google OAuth 콜백 URL 환경변수화 + +로컬 주소를 코드에 고정하면 EC2 배포 후에도 Google이 `localhost`로 이동한다. 따라서 `src/auth.config.ts`에서 콜백 URL을 환경변수로 받도록 변경했다. + +```ts +callbackURL: + process.env.PASSPORT_GOOGLE_CALLBACK_URL ?? + "http://localhost:3000/oauth2/callback/google"; +``` + +로컬 환경에서는 다음 값을 사용한다. + +```dotenv +PASSPORT_GOOGLE_CALLBACK_URL=http://localhost:3000/oauth2/callback/google +``` + +배포 환경에서는 Google OAuth 정책에 맞춰 HTTPS 도메인을 사용한다. + +```dotenv +PASSPORT_GOOGLE_CALLBACK_URL=https:///oauth2/callback/google +``` + +> 실제 서비스에서는 도메인과 HTTPS를 적용한 `https://api.example.com/oauth2/callback/google` 형태를 사용한다. + +## 5. EC2 준비 + +### 5-1. 인스턴스와 네트워크 + +- [x] Ubuntu EC2 인스턴스 생성 +- [x] 탄력적 IP 연결 +- [x] 보안 그룹에서 SSH 22번 포트를 내 IP에만 허용 +- [x] Nginx용 HTTP 80, HTTPS 443 포트 허용 +- [x] 애플리케이션 3000번 외부 규칙 제거 +- [x] SSH 접속 확인 + +운영 환경에서는 애플리케이션의 3000번 포트를 직접 공개하지 않고, Nginx가 80/443번 요청을 받아 내부 3000번 포트로 전달하도록 구성하는 것이 좋다. + +### 5-2. Node.js 24 설치 + +```bash +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +source ~/.bashrc +nvm install 24 +nvm use 24 +node -v +npm -v +``` + +### 5-3. MySQL 설치 + +워크북 실습에서는 비용과 구성을 단순화하기 위해 같은 EC2에 MySQL을 설치한다. + +```bash +sudo apt update +sudo apt install -y mysql-server +sudo systemctl enable --now mysql +sudo mysql -u root +``` + +```sql +CREATE DATABASE umc_10th + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; +CREATE USER 'umc_app'@'localhost' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON umc_10th.* TO 'umc_app'@'localhost'; +FLUSH PRIVILEGES; +EXIT; +``` + +- [x] MySQL 서비스가 `active`인지 확인 +- [x] `umc_10th` 데이터베이스 생성 +- [x] `umc_app` 전용 사용자 생성 및 접속 확인 +- [x] 비밀번호를 공개 문서나 저장소에 기록하지 않기 + +메모리가 약 1GiB인 인스턴스에서 Prisma migration과 MySQL을 함께 실행할 수 있도록 1GiB swap을 추가했다. + +```bash +sudo fallocate -l 1G /swapfile +sudo chmod 600 /swapfile +sudo mkswap /swapfile +sudo swapon /swapfile +``` + +## 6. GitHub Actions Secrets 설정 + +GitHub 저장소의 `Settings → Secrets and variables → Actions`에서 다음 Repository secrets를 등록한다. + +| Secret | 값 | +| --- | --- | +| `EC2_HOST` | EC2 탄력적 IP | +| `EC2_PORT` | SSH 포트, 기본값 `22` | +| `EC2_USER` | Ubuntu AMI 기준 `ubuntu` | +| `EC2_SSH_KEY` | EC2 생성 시 받은 PEM 파일 전체 내용 | +| `EC2_DOT_ENV` | EC2에서 사용할 `.env` 전체 내용 | + +`EC2_DOT_ENV` 예시: + +```dotenv +PORT=3000 +DATABASE_URL=mysql://umc_app:@localhost:3306/umc_10th +DB_HOST=localhost +DB_PORT=3306 +DB_USER=umc_app +DB_PASSWORD= +DB_NAME=umc_10th +PASSPORT_GOOGLE_CLIENT_ID= +PASSPORT_GOOGLE_CLIENT_SECRET= +PASSPORT_GOOGLE_CALLBACK_URL=https:///oauth2/callback/google +JWT_SECRET= +``` + +주의 사항: + +- 실제 Secret 값을 README, 커밋, Actions 로그에 붙이지 않는다. +- PEM 키는 시작 줄과 끝 줄을 포함한 전체 내용을 등록한다. +- DB 비밀번호에 URL 예약 문자가 있다면 `DATABASE_URL`에서 URL 인코딩한다. + +## 7. GitHub Actions 워크플로 + +워크플로 파일은 `.github/workflows/deploy-main.yml`에 작성했다. + +### build job + +1. 저장소 checkout +2. Node.js 24 설정 +3. `npm ci`로 잠금 파일과 같은 의존성 설치 +4. 빌드용 `.env` 생성 +5. `npm run build` +6. 개발 의존성 제거 +7. `dist`, `node_modules`, 패키지 파일을 아티팩트로 압축 + +### deploy job + +1. build job의 아티팩트 다운로드 +2. SSH 개인 키와 known hosts 설정 +3. `/opt/app/umc10th/incoming`으로 파일 전송 +4. Secret을 이용해 런타임 `.env` 생성 +5. Prisma migration 적용 +6. 기존 `current`를 `previous`로 옮기고 새 릴리스를 `current`로 전환 +7. `umc10th.service` 등록 및 재시작 +8. systemd 상태와 `http://127.0.0.1:3000/` 응답 확인 + +## 8. 첫 배포 + +1. Chapter 10 변경사항을 원격 브랜치에 push한다. +2. Pull Request를 통해 `main`에 병합한다. +3. GitHub의 Actions 탭에서 `deploy-main`을 연다. +4. build와 deploy job이 모두 초록색인지 확인한다. +5. EC2에서 다음 명령으로 상태를 확인한다. + +```bash +sudo systemctl status umc10th --no-pager +sudo journalctl -u umc10th -n 100 --no-pager +curl http://127.0.0.1:3000/ +``` + +외부에서는 Nginx와 HTTPS 도메인을 통해 확인한다. + +```text +https:/// +``` + +- [x] Actions build 성공 +- [x] Actions deploy 성공 +- [x] EC2 systemd `active` 확인 +- [x] 외부 HTTPS API 응답 확인 + +### Nginx와 HTTPS + +DuckDNS 서브도메인을 탄력적 IP에 연결하고 Nginx가 80/443 요청을 내부 3000번 포트로 전달하도록 구성했다. Certbot으로 TLS 인증서를 발급했으며 `certbot renew --dry-run`도 성공했다. + +```text +Client → HTTPS :443 → Nginx → HTTP 127.0.0.1:3000 → Node.js +``` + +## 9. 배포 환경에서 Google 로그인 수정 + +Google Cloud Console의 OAuth Client 설정에서 승인된 리디렉션 URI를 추가한다. + +```text +https:///oauth2/callback/google +``` + +그다음 브라우저에서 아래 주소로 접속한다. + +```text +https:///oauth2/login/google +``` + +- [x] Google 로그인 화면으로 이동 +- [x] 로그인 후 HTTPS 콜백 주소로 복귀 +- [x] Access Token과 Refresh Token 응답 확인 +- [x] Access Token으로 `/mypage` 호출 성공 + +## 10. 실습 인증 이미지 계획 + +실제 진행 중 아래 이름으로 이미지를 추가한다. + +```text +misson_practice/chapter10/images/ +├── 01-ec2-instance.png +├── 02-elastic-ip.png +├── 03-node-mysql-install.png +├── 04-github-secrets.png +├── 05-actions-build-success.png +├── 06-actions-deploy-success.png +├── 07-systemd-active.png +├── 08-api-response.png +├── 09-google-callback-setting.png +└── 10-google-login-success.png +``` + +Secret 값, PEM 키, DB 비밀번호, 토큰은 반드시 가리고 캡처한다. + +## 11. 트러블슈팅 + +### 이슈 1: 워크북 YAML의 `npm run build`가 실패함 + +**문제** + +기존 프로젝트는 `tsx src/index.ts`로 바로 실행했으며 `build` 스크립트가 없었다. 워크북 예제는 `dist` 디렉터리가 이미 생성된다고 가정하고 있었다. + +**해결** + +`prisma generate → tsoa spec-and-routes → tsc` 순서의 build 스크립트를 만들고, start는 `node dist/index.js`를 실행하도록 분리했다. + +### 이슈 2: TSOA 생성 라우트가 ESM 빌드에서 해석되지 않음 + +**문제** + +TSOA가 생성한 import 경로에는 `.js` 확장자가 없어서 NodeNext 모듈 해석에서 컴파일 오류가 발생했다. + +**해결** + +현재 프로젝트의 TSOA 생성 방식에 맞춰 TypeScript 출력과 Node 런타임 모듈 형식을 CommonJS로 통일했다. + +### 이슈 3: 배포 후 Google 로그인이 localhost로 이동함 + +**문제** + +Google Strategy의 callback URL이 `http://localhost:3000`으로 하드코딩되어 있었다. + +**해결** + +`PASSPORT_GOOGLE_CALLBACK_URL` 환경변수를 추가하고 Google Cloud Console에도 HTTPS 도메인 콜백 URI를 등록했다. + +### 이슈 4: Repository secrets가 없어 빌드 환경변수가 비어 있음 + +**문제** + +GitHub Actions의 Prisma generate 단계에서 `DATABASE_URL`을 찾을 수 없었다. 확인 결과 Repository secrets가 등록되지 않은 상태였다. + +**해결** + +실습 저장소의 Repository secrets에 `EC2_HOST`, `EC2_PORT`, `EC2_USER`, `EC2_SSH_KEY`, `EC2_DOT_ENV`를 등록했다. + +### 이슈 5: Prisma가 MySQL 인증 오류를 다른 plugin 오류로 표시함 + +**문제** + +DB 비밀번호 불일치 상황에서 Prisma가 `Unknown authentication plugin sha256_password`를 출력해 원인 파악이 어려웠다. + +**해결** + +`umc_app` 비밀번호를 재설정하고 같은 값을 `DATABASE_URL`, `DB_PASSWORD`에 반영했다. MySQL 인증 방식은 `mysql_native_password`로 통일했다. + +### 이슈 6: 작은 EC2에서 migration 중 DB 연결 실패 + +**문제** + +MySQL과 Node.js가 실행 중인 약 1GiB 인스턴스의 가용 메모리가 100MiB 미만이었고 swap도 없었다. Prisma migration 중 일시적으로 `P1001` 연결 실패가 발생했다. + +**해결** + +기존 EBS 볼륨에 1GiB swap을 만들고 migration을 다시 실행했다. 이후 migration, systemd 재시작, HTTP 검증이 모두 통과했다. + +### 이슈 7: Google OAuth는 공인 IP 기반 HTTP callback을 허용하지 않음 + +**문제** + +Google OAuth는 localhost를 제외한 HTTP callback과 원시 IP host를 허용하지 않는다. + +**해결** + +DuckDNS 도메인, Nginx, Certbot을 적용하고 Google Cloud Console에 HTTPS callback URI를 등록했다. + +## 12. 시니어 미션: 무중단 배포 + +현재 systemd의 `restart` 방식은 짧은 다운타임이 생긴다. 기본 미션을 완료한 후 PM2 cluster mode와 `pm2 reload`를 적용하면 기존 프로세스가 요청을 처리하는 동안 새 프로세스를 띄워 다운타임을 줄일 수 있다. + +추가로 확인할 항목: + +- `ecosystem.config.cjs` 작성 +- PM2 cluster mode 적용 +- `pm2 startup`과 `pm2 save` 설정 +- Actions 배포 단계에서 `pm2 reload` 실행 +- 연속 요청 중 실패가 없는지 검증 + +## 13. 회고 + +이번 실습에서 CI와 CD를 하나의 막연한 자동화가 아니라 build와 deploy라는 서로 다른 책임으로 나누어 이해할 수 있었다. 특히 GitHub Actions에서 한 번 만든 아티팩트를 EC2로 전달하므로, 작은 EC2 인스턴스가 직접 의존성을 설치하고 TypeScript를 컴파일하는 부담을 줄일 수 있었다. + +또한 로컬에서 정상 동작하던 OAuth도 배포 주소와 Google Console 설정이 함께 바뀌어야 한다는 점을 확인했다. 코드, 인프라, 외부 서비스 설정이 모두 일치해야 실제 배포가 완성된다. diff --git a/package-lock.json b/package-lock.json index a0b2bb8..17c3037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,76 @@ { - "name": "umc_10", + "name": "10th_node", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "umc_10", + "name": "10th_node", "version": "1.0.0", "license": "ISC", "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", "express": "^5.2.1", - "nodemon": "^3.1.14", - "tsx": "^4.21.0" + "jsonwebtoken": "^9.0.3", + "morgan": "^1.10.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "prisma": "^7.8.0", + "tsoa": "^7.0.0-alpha.0" }, "devDependencies": { - "@types/express": "^5.0.6" + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/passport": "^1.0.17", + "@types/passport-google-oauth20": "^2.0.17", + "@types/passport-jwt": "^4.0.1", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -34,12 +81,13 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -50,12 +98,13 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -66,12 +115,13 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -82,12 +132,13 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -98,12 +149,13 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -114,12 +166,13 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -130,12 +183,13 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -146,12 +200,13 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -162,12 +217,13 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -178,12 +234,13 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -194,12 +251,13 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -210,12 +268,13 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -226,12 +285,13 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -242,12 +302,13 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -258,12 +319,13 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -274,12 +336,13 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -290,12 +353,13 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -306,12 +370,13 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -322,12 +387,13 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -338,12 +404,13 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -354,12 +421,13 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -370,12 +438,13 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -386,12 +455,13 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -402,12 +472,13 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -418,12 +489,13 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -433,265 +505,1805 @@ "node": ">=18" } }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "license": "BSD-3-Clause", "dependencies": { - "@types/node": "*" + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "license": "BSD-3-Clause", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/bounce": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz", + "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==", + "license": "BSD-3-Clause", "dependencies": { - "undici-types": "~7.18.0" + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", - "dev": true, - "license": "MIT" + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/catbox-memory": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz", + "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==", + "license": "BSD-3-Clause", "dependencies": { - "@types/node": "*" + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/content": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.2.tgz", + "integrity": "sha512-OKyCOTjNR1hftwSjk9ueyAQTw8AwapvzBrPIWMGn39vhR5PmqLdYFmLc35bsSBye7gSMnlkXfc679bUdMIcRyQ==", + "license": "BSD-3-Clause", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" + "@hapi/boom": "^10.0.0" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", + "node_modules/@hapi/cryptiles": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz", + "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==", + "license": "BSD-3-Clause", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "@hapi/boom": "^10.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=14.0.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hapi": { + "version": "21.4.9", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.9.tgz", + "integrity": "sha512-YnecZOVx2AD08VvPl0ZaFS0MjEHqg+InGRmBRli731ct+VwI++dpu3BIYA1Z4SMr6HUAnpyvbQ1aq5woe3fBWg==", + "license": "BSD-3-Clause", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@hapi/accept": "^6.0.3", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.2", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.2", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.2", + "@hapi/shot": "^6.0.2", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.2.1", + "@hapi/subtext": "^8.1.3", + "@hapi/teamwork": "^6.0.1", + "@hapi/topo": "^6.0.2", + "@hapi/validate": "^2.0.1" }, "engines": { - "node": ">= 8" + "node": ">=14.15.0" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "license": "BSD-3-Clause", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" } }, - "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "license": "MIT", + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "license": "BSD-3-Clause", "dependencies": { - "balanced-match": "^4.0.2" + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=14.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", + "node_modules/@hapi/pez": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz", + "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==", + "license": "BSD-3-Clause", "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/nigel": "^5.0.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/@hapi/podium": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz", + "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", + "node_modules/@hapi/shot": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz", + "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==", + "license": "BSD-3-Clause", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "license": "BSD-3-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", + "node_modules/@hapi/statehood": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz", + "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==", + "license": "BSD-3-Clause", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" } }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "node_modules/@hapi/subtext": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.3.tgz", + "integrity": "sha512-WTpEZQjBP3UJ3gGunNl3w5Ao1EOJsuu2vttZ2KEcG+csSLxc0dI6VIkl2md2jDlHiQ2ARAoqdSUScy05A/NHtA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.2", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.7", + "@hapi/pez": "^6.1.1", + "@hapi/wreck": "^18.1.1" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz", + "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.1.tgz", + "integrity": "sha512-UwTeGBfAnB/1mkw4gD6IQGI/bgMu7iGmqgT8K+xxye3z4ZHhCZlmS2wuHBJmENhBJSKqvoYzJ71ds3Xfq4gofQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/adapter-mariadb": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.8.0.tgz", + "integrity": "sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "mariadb": "3.4.5" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tsoa/cli": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-7.0.0-alpha.0.tgz", + "integrity": "sha512-fCBWv6F20qrpwFh5X+vaZs38Bh/5cVwKPhvG/uArR+crZgEowyNl76unLCmYdvycES9Y6g/TKFLagG8qojSNuQ==", + "license": "MIT", + "dependencies": { + "@tsoa/runtime": "^7.0.0-alpha.0", + "@types/multer": "^1.4.12", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "merge-anything": "^5.1.7", + "minimatch": "^9.0.1", + "ts-deepmerge": "^7.0.2", + "typescript": "^5.7.2", + "validator": "^13.12.0", + "yaml": "^2.6.1", + "yargs": "^17.7.1" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/cli/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/@tsoa/cli/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tsoa/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsoa/runtime": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-7.0.0-alpha.0.tgz", + "integrity": "sha512-zlWYz2bLfaN6WtFoIbLBEAyVhKG4IQKJ9QPzeRFFKeAsh5zYN4/ocnd14XyWs4ehY9TdtTnma2drW89aYNSRYw==", + "license": "MIT", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hapi": "^21.3.12", + "@types/koa": "^2.15.0", + "@types/multer": "^1.4.12", + "express": "^4.21.2", + "reflect-metadata": "^0.2.2", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/runtime/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@tsoa/runtime/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@tsoa/runtime/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@tsoa/runtime/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT" + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz", + "integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/better-result": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", + "integrity": "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", "engines": { "node": ">=18" @@ -719,6 +2331,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -728,6 +2359,44 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT", + "peer": true + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -745,6 +2414,30 @@ } } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -754,6 +2447,34 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -768,12 +2489,52 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -783,6 +2544,18 @@ "node": ">= 0.8" } }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -814,9 +2587,10 @@ } }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -826,32 +2600,41 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/escape-html": { @@ -912,10 +2695,61 @@ "url": "https://opencollective.com/express" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -945,6 +2779,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -963,10 +2813,25 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -986,6 +2851,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1010,6 +2893,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "license": "MIT" + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -1024,9 +2913,10 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -1035,10 +2925,41 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -1047,6 +2968,36 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1059,10 +3010,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -1092,6 +3083,15 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1112,6 +3112,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1132,6 +3138,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, "license": "ISC" }, "node_modules/inherits": { @@ -1153,6 +3160,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -1161,42 +3169,260 @@ "node": ">=8" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", + "node_modules/mariadb": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz", + "integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==", + "license": "LGPL-2.1-or-later", "dependencies": { - "is-extglob": "^2.1.1" + "@types/geojson": "^7946.0.16", + "@types/node": "^24.0.13", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.4.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 14" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/mariadb/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=0.10.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1215,6 +3441,21 @@ "node": ">= 0.8" } }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -1227,6 +3468,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1253,12 +3515,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -1267,12 +3530,105 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -1282,10 +3638,17 @@ "node": ">= 0.6" } }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", @@ -1314,6 +3677,22 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -1331,6 +3710,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -1343,6 +3728,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1352,6 +3746,12 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1361,20 +3761,131 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz", + "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1383,6 +3894,80 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1400,12 +3985,29 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], "license": "MIT" }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -1441,10 +4043,44 @@ "node": ">= 0.10" } }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -1453,15 +4089,58 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1478,12 +4157,25 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "peer": true + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -1522,6 +4214,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -1547,6 +4244,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1619,10 +4337,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -1631,6 +4362,24 @@ "node": ">=10" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1640,10 +4389,113 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -1656,6 +4508,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -1677,15 +4530,43 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, + "node_modules/ts-deepmerge": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz", + "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/tsoa": { + "version": "7.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-7.0.0-alpha.0.tgz", + "integrity": "sha512-o5h2DD1IKa2GF728BHYDL2uSX57a44sX8BLFScR4KbD0xlOmgsSDECneJmouDxf9MJdjHHWc+I1S3sGK82B6kQ==", + "license": "MIT", + "dependencies": { + "@tsoa/cli": "^7.0.0-alpha.0", + "@tsoa/runtime": "^7.0.0-alpha.0" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -1715,19 +4596,60 @@ "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, "license": "MIT" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1737,6 +4659,38 @@ "node": ">= 0.8" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1746,11 +4700,225 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } } } } diff --git a/package.json b/package.json index 76b6bb7..ada7520 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,51 @@ { - "name": "umc_10", + "name": "10th_node", "version": "1.0.0", - "description": "UMC 10기 Node.js 실습 레포입니다.", + "description": "INHA UMC 10th_Node.js 미션, 키워드 인증 레포지터리입니다!", "main": "index.js", "scripts": { - "start:dev": "nodemon --watch '*.ts' --exec tsx ./src/app.ts", - "build": "tsc" + "build": "prisma generate --schema=./prisma/schema.prisma && tsoa spec-and-routes && tsc", + "start": "node dist/index.js", + "dev": "prisma generate --schema=./prisma/schema.prisma && tsoa spec-and-routes && nodemon --exec tsx src/index.ts", + "migrate:deploy": "prisma migrate deploy --schema=./prisma/schema.prisma" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/inhadissolve/10th_Node.js.git" }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs", + "bugs": { + "url": "https://github.com/inhadissolve/10th_Node.js/issues" + }, + "homepage": "https://github.com/inhadissolve/10th_Node.js#readme", "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", "express": "^5.2.1", - "nodemon": "^3.1.14", - "tsx": "^4.21.0" + "jsonwebtoken": "^9.0.3", + "morgan": "^1.10.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "prisma": "^7.8.0", + "tsoa": "^7.0.0-alpha.0" }, "devDependencies": { - "@types/express": "^5.0.6" + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/passport": "^1.0.17", + "@types/passport-google-oauth20": "^2.0.17", + "@types/passport-jwt": "^4.0.1", + "nodemon": "^3.1.14", + "tsx": "^4.21.0", + "typescript": "^5.9.3" } } diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..63db9d9 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); \ No newline at end of file diff --git a/prisma/migrations/20260506151555_init/migration.sql b/prisma/migrations/20260506151555_init/migration.sql new file mode 100644 index 0000000..529a88e --- /dev/null +++ b/prisma/migrations/20260506151555_init/migration.sql @@ -0,0 +1,126 @@ +-- CreateTable +CREATE TABLE `user` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL, + `password` VARCHAR(255) NULL, + `name` VARCHAR(100) NOT NULL, + `gender` VARCHAR(15) NOT NULL, + `birth` DATE NOT NULL, + `address` VARCHAR(255) NOT NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(20) NOT NULL, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `food_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_favor_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + INDEX `user_id`(`user_id`), + INDEX `f_category_id`(`food_category_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `regions` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(50) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `stores` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `region_id` INTEGER NOT NULL, + `name` VARCHAR(100) NOT NULL, + `address` VARCHAR(255) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `region_id`(`region_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `reviews` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `store_id` INTEGER NOT NULL, + `rating` INTEGER NOT NULL, + `content` TEXT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `user_id`(`user_id`), + INDEX `store_id`(`store_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `missions` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `title` VARCHAR(100) NOT NULL, + `description` TEXT NULL, + `reward_point` INTEGER NOT NULL DEFAULT 0, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `store_id`(`store_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_missions` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `mission_id` INTEGER NOT NULL, + `status` VARCHAR(20) NOT NULL DEFAULT 'IN_PROGRESS', + `started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `completed_at` DATETIME(3) NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `user_id`(`user_id`), + INDEX `mission_id`(`mission_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `stores` ADD CONSTRAINT `stores_region_id_fkey` FOREIGN KEY (`region_id`) REFERENCES `regions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `reviews` ADD CONSTRAINT `reviews_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `reviews` ADD CONSTRAINT `reviews_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `stores`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `missions` ADD CONSTRAINT `missions_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `stores`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_missions` ADD CONSTRAINT `user_missions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_missions` ADD CONSTRAINT `user_missions_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `missions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..592fc0b --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..e0903d6 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,126 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "mysql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + password String? @db.VarChar(255) + name String @db.VarChar(100) + gender String @db.VarChar(15) + birth DateTime @db.Date + address String @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String @map("phone_number") @db.VarChar(20) + + userFavorCategories UserFavorCategory[] + reviews Review[] + userMissions UserMission[] + + @@map("user") +} + +model FoodCategory { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + userFavorCategories UserFavorCategory[] + + @@map("food_category") +} + +model UserFavorCategory { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + foodCategoryId Int @map("food_category_id") + + user User @relation(fields: [userId], references: [id]) + foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) + + @@index([userId], map: "user_id") + @@index([foodCategoryId], map: "f_category_id") + @@map("user_favor_category") +} + +model Region { + id Int @id @default(autoincrement()) + name String @db.VarChar(50) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + + stores Store[] + + @@map("regions") +} + +model Store { + id Int @id @default(autoincrement()) + regionId Int @map("region_id") + name String @db.VarChar(100) + address String @db.VarChar(255) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + + region Region @relation(fields: [regionId], references: [id]) + reviews Review[] + missions Mission[] + + @@index([regionId], map: "region_id") + @@map("stores") +} + +model Review { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + storeId Int @map("store_id") + rating Int + content String? @db.Text + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + store Store @relation(fields: [storeId], references: [id]) + + @@index([userId], map: "user_id") + @@index([storeId], map: "store_id") + @@map("reviews") +} + +model Mission { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + title String @db.VarChar(100) + description String? @db.Text + rewardPoint Int @default(0) @map("reward_point") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + + store Store @relation(fields: [storeId], references: [id]) + userMissions UserMission[] + + @@index([storeId], map: "store_id") + @@map("missions") +} + +model UserMission { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + missionId Int @map("mission_id") + status String @default("IN_PROGRESS") @db.VarChar(20) + startedAt DateTime @default(now()) @map("started_at") + completedAt DateTime? @map("completed_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @map("updated_at") + + user User @relation(fields: [userId], references: [id]) + mission Mission @relation(fields: [missionId], references: [id]) + + @@index([userId], map: "user_id") + @@index([missionId], map: "mission_id") + @@map("user_missions") +} \ No newline at end of file diff --git a/src/auth.config.ts b/src/auth.config.ts new file mode 100644 index 0000000..f066754 --- /dev/null +++ b/src/auth.config.ts @@ -0,0 +1,143 @@ +import dotenv from "dotenv"; +import { Strategy as GoogleStrategy, Profile } from "passport-google-oauth20"; +import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; +import jwt from "jsonwebtoken"; +import { prisma } from "./db.config.js"; + +dotenv.config(); + +/** + * Access Token 생성 + * + * Access Token은 일반 API 요청에 사용하는 짧은 수명의 토큰입니다. + * Payload에는 사용자를 식별할 수 있는 최소 정보만 담습니다. + */ +export const generateAccessToken = (user: { id: number; email: string }) => { + return jwt.sign( + { + id: user.id, + email: user.email, + }, + process.env.JWT_SECRET!, + { + expiresIn: "1h", + } + ); +}; + +/** + * Refresh Token 생성 + * + * Refresh Token은 Access Token을 재발급받기 위한 긴 수명의 토큰입니다. + * 여기서는 최소 정보인 user id만 담습니다. + */ +export const generateRefreshToken = (user: { id: number }) => { + return jwt.sign( + { + id: user.id, + }, + process.env.JWT_SECRET!, + { + expiresIn: "14d", + } + ); +}; + +/** + * Google 로그인 후 사용자 조회 또는 생성 + * + * Google에서 전달받은 profile 정보 중 이메일을 기준으로 + * 기존 사용자가 있으면 조회하고, 없으면 새로 생성합니다. + */ +const googleVerify = async (profile: Profile) => { + const email = profile.emails?.[0]?.value; + + if (!email) { + throw new Error("Google 프로필에 이메일이 없습니다."); + } + + let user = await prisma.user.findFirst({ + where: { email }, + }); + + if (!user) { + user = await prisma.user.create({ + data: { + email, + name: profile.displayName, + gender: "추후 수정", + birth: new Date(1970, 0, 1), + address: "추후 수정", + detailAddress: "추후 수정", + phoneNumber: "추후 수정", + }, + }); + } + + return { + id: user.id, + email: user.email, + name: user.name, + }; +}; + +/** + * Google OAuth Strategy + * + * /oauth2/login/google 로 접속하면 Google 로그인 화면으로 이동하고, + * 로그인 성공 후 /oauth2/callback/google 로 돌아옵니다. + */ +export const googleStrategy = new GoogleStrategy( + { + clientID: process.env.PASSPORT_GOOGLE_CLIENT_ID!, + clientSecret: process.env.PASSPORT_GOOGLE_CLIENT_SECRET!, + callbackURL: + process.env.PASSPORT_GOOGLE_CALLBACK_URL ?? + "http://localhost:3000/oauth2/callback/google", + scope: ["email", "profile"], + }, + async (_accessToken, _refreshToken, profile, cb) => { + try { + const user = await googleVerify(profile); + + const tokens = { + accessToken: generateAccessToken(user), + refreshToken: generateRefreshToken(user), + }; + + return cb(null, tokens); + } catch (err) { + return cb(err as Error); + } + } +); + +/** + * JWT Strategy + * + * Authorization 헤더에 담긴 Bearer Token을 검증합니다. + * 토큰이 유효하면 req.user에 사용자 정보를 넣어줍니다. + */ +export const jwtStrategy = new JwtStrategy( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET!, + }, + async (payload: { id: number; email?: string }, done) => { + try { + const user = await prisma.user.findFirst({ + where: { + id: payload.id, + }, + }); + + if (!user) { + return done(null, false); + } + + return done(null, user); + } catch (err) { + return done(err, false); + } + } +); diff --git a/src/common/errors/app.error.ts b/src/common/errors/app.error.ts new file mode 100644 index 0000000..2b2d955 --- /dev/null +++ b/src/common/errors/app.error.ts @@ -0,0 +1,18 @@ +export class AppError extends Error { + public readonly errorCode: string; + public readonly statusCode: number; + public readonly data?: unknown; + + constructor(params: { + errorCode: string; + message: string; + statusCode: number; + data?: unknown; + }) { + super(params.message); + + this.errorCode = params.errorCode; + this.statusCode = params.statusCode; + this.data = params.data ?? null; + } +} \ No newline at end of file diff --git a/src/common/errors/user.error.ts b/src/common/errors/user.error.ts new file mode 100644 index 0000000..0202ff8 --- /dev/null +++ b/src/common/errors/user.error.ts @@ -0,0 +1,12 @@ +import { AppError } from "./app.error.js"; + +export class DuplicateUserEmailError extends AppError { + constructor(data?: unknown) { + super({ + errorCode: "U001", + message: "이미 존재하는 이메일입니다.", + statusCode: 409, + data, + }); + } +} \ No newline at end of file diff --git a/src/common/middlewares/auth.middleware.ts b/src/common/middlewares/auth.middleware.ts new file mode 100644 index 0000000..deab7dd --- /dev/null +++ b/src/common/middlewares/auth.middleware.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from "express"; + +export function authorizeUser() { + return async (req: Request, res: Response, next: NextFunction) => { + const { username } = req.cookies; + + if (username) { + console.log(`[인증 성공] ${username}님, 환영합니다.`); + next(); + return; + } + + console.log("[인증 실패] 로그인이 필요합니다."); + + res + .status(401) + .send( + '' + ); + }; +} \ No newline at end of file diff --git a/src/common/responses/response.ts b/src/common/responses/response.ts new file mode 100644 index 0000000..9469334 --- /dev/null +++ b/src/common/responses/response.ts @@ -0,0 +1,11 @@ +export interface ApiResponse { + resultType: "SUCCESS"; + error: null; + success: T; +} + +export const success = (data: T): ApiResponse => ({ + resultType: "SUCCESS", + error: null, + success: data, +}); \ No newline at end of file diff --git a/src/db.config.ts b/src/db.config.ts new file mode 100644 index 0000000..08b3acd --- /dev/null +++ b/src/db.config.ts @@ -0,0 +1,17 @@ +import "dotenv/config"; +import { PrismaClient } from "./generated/prisma/client.js"; +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; + +const adapter = new PrismaMariaDb({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, + connectionLimit: 10, +}); + +export const prisma = new PrismaClient({ + adapter, + log: ["query", "info", "warn", "error"], +}); \ No newline at end of file diff --git a/src/generated/routes.ts b/src/generated/routes.ts new file mode 100644 index 0000000..9fff9b6 --- /dev/null +++ b/src/generated/routes.ts @@ -0,0 +1,252 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import type { TsoaRoute } from '@tsoa/runtime'; +import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { UserController } from './../modules/users/controllers/user.controller'; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; + + + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "UserSignUpResponse": { + "dataType": "refObject", + "properties": { + "userId": {"dataType":"double","required":true}, + "preferences": {"dataType":"array","array":{"dataType":"string"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_UserSignUpResponse_": { + "dataType": "refObject", + "properties": { + "resultType": {"dataType":"enum","enums":["SUCCESS"],"required":true}, + "error": {"dataType":"enum","enums":[null],"required":true}, + "success": {"ref":"UserSignUpResponse","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserSignUpRequest": { + "dataType": "refObject", + "properties": { + "email": {"dataType":"string","required":true}, + "password": {"dataType":"string"}, + "name": {"dataType":"string","required":true}, + "gender": {"dataType":"string","required":true}, + "birth": {"dataType":"string","required":true}, + "address": {"dataType":"string"}, + "detailAddress": {"dataType":"string"}, + "phoneNumber": {"dataType":"string","required":true}, + "preferences": {"dataType":"array","array":{"dataType":"double"},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + + +export function RegisterRoutes(app: Router) { + + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + + + const argsUserController_handleUserSignUp: Record = { + body: {"in":"body","name":"body","required":true,"ref":"UserSignUpRequest"}, + }; + app.post('/users/signup', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleUserSignUp)), + + async function UserController_handleUserSignUp(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleUserSignUp, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleUserSignUp', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleGuestPage: Record = { + }; + app.get('/users/guest', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleGuestPage)), + + async function UserController_handleGuestPage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleGuestPage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleGuestPage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleLoginPage: Record = { + }; + app.get('/users/login', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleLoginPage)), + + async function UserController_handleLoginPage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleLoginPage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleLoginPage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleMypage: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/mypage', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleMypage)), + + async function UserController_handleMypage(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleMypage, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleMypage', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleSetLogin: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/set-login', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleSetLogin)), + + async function UserController_handleSetLogin(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleSetLogin, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleSetLogin', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleSetLogout: Record = { + req: {"in":"request","name":"req","required":true,"dataType":"object"}, + }; + app.get('/users/set-logout', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleSetLogout)), + + async function UserController_handleSetLogout(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleSetLogout, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleSetLogout', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..eb44365 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,149 @@ +import dotenv from "dotenv"; +import express, { Express, NextFunction, Request, Response } from "express"; +import cors from "cors"; +import morgan from "morgan"; +import cookieParser from "cookie-parser"; +import passport from "passport"; + +import { RegisterRoutes } from "./generated/routes.js"; +import { AppError } from "./common/errors/app.error.js"; +import { googleStrategy, jwtStrategy } from "./auth.config.js"; + +dotenv.config(); + +/** + * Passport Strategy 등록 + * + * googleStrategy: Google OAuth 로그인 처리 + * jwtStrategy: Bearer Token 기반 JWT 인증 처리 + */ +passport.use(googleStrategy); +passport.use(jwtStrategy); + +const app: Express = express(); +const port = process.env.PORT || 3000; + +/** + * 공통 미들웨어 설정 + */ +app.use(cors()); +app.use(morgan("dev")); +app.use(cookieParser()); +app.use(express.static("public")); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +/** + * Passport 초기화 + * + * session: false 방식으로 사용할 예정이기 때문에 + * express-session은 따로 사용하지 않습니다. + */ +app.use(passport.initialize()); + +/** + * 기본 서버 확인용 API + */ +app.get("/", (req: Request, res: Response) => { + res.send("Hello World! This is TypeScript Server!"); +}); + +/** + * Google 로그인 시작 라우트 + * + * 브라우저에서 아래 주소로 접속하면 Google 로그인 화면으로 이동합니다. + * http://localhost:3000/oauth2/login/google + */ +app.get( + "/oauth2/login/google", + passport.authenticate("google", { session: false }) +); + +/** + * Google 로그인 Callback 라우트 + * + * Google 로그인이 성공하면 이 주소로 돌아옵니다. + * 성공 시 accessToken, refreshToken을 JSON으로 응답합니다. + */ +app.get( + "/oauth2/callback/google", + passport.authenticate("google", { + session: false, + failureRedirect: "/login-failed", + }), + (req: Request, res: Response) => { + res.status(200).json({ + success: true, + tokens: req.user, + }); + } +); + +/** + * Google 로그인 실패 라우트 + */ +app.get("/login-failed", (req: Request, res: Response) => { + res.status(401).json({ + success: false, + message: "Google 로그인에 실패했습니다.", + }); +}); + +/** + * JWT 인증 미들웨어 + * + * Authorization: Bearer + * 형식으로 토큰을 보내야 통과할 수 있습니다. + */ +const isLogin = passport.authenticate("jwt", { session: false }); + +/** + * JWT 인증 테스트용 보호 라우트 + * + * accessToken이 있으면 성공하고, + * 없거나 잘못된 토큰이면 401 Unauthorized가 발생합니다. + */ +app.get("/mypage", isLogin, (req: Request, res: Response) => { + res.status(200).json({ + success: true, + message: "인증 성공! 마이페이지에 접근했습니다.", + user: req.user, + }); +}); + +/** + * Tsoa가 자동 생성한 routes 등록 + * + * 기존 API들은 /api/v1 경로 아래에서 동작합니다. + */ +const router = express.Router(); + +RegisterRoutes(router); + +app.use("/api/v1", router); + +/** + * 전역 에러 핸들러 + */ +app.use((err: AppError, req: Request, res: Response, next: NextFunction) => { + if (res.headersSent) { + return next(err); + } + + res.status(err.statusCode || 500).json({ + resultType: "FAIL", + error: { + errorCode: err.errorCode || "UNKNOWN", + reason: err.message || "서버 오류가 발생했습니다.", + data: err.data || null, + }, + success: null, + }); +}); + +/** + * 서버 실행 + */ +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`); +}); \ No newline at end of file diff --git a/src/modules/missions/controllers/mission.controller.ts b/src/modules/missions/controllers/mission.controller.ts new file mode 100644 index 0000000..e56a675 --- /dev/null +++ b/src/modules/missions/controllers/mission.controller.ts @@ -0,0 +1,153 @@ +import { Request, Response, NextFunction } from "express"; +import { StatusCodes } from "http-status-codes"; +import { + bodyToMission, + bodyToMissionChallenge, +} from "../dtos/mission.dto.js"; + +import { + createMission, + challengeMission, + listStoreMissions, + listInProgressMissions, + completeMission, +} from "../services/mission.service.js"; + +export const handleCreateMission = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + console.log("미션 추가를 요청했습니다."); + console.log("params:", req.params); + console.log("body:", req.body); + + const storeId = Number(req.params.storeId); + + if (Number.isNaN(storeId)) { + throw new Error("storeId는 숫자여야 합니다."); + } + + const mission = await createMission(bodyToMission(req.body, storeId)); + + res.status(StatusCodes.CREATED).json({ + result: mission, + }); + } catch (error) { + next(error); + } +}; + +export const handleChallengeMission = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + console.log("미션 도전을 요청했습니다."); + console.log("params:", req.params); + console.log("body:", req.body); + + const missionId = Number(req.params.missionId); + + if (Number.isNaN(missionId)) { + throw new Error("missionId는 숫자여야 합니다."); + } + + const userMission = await challengeMission( + bodyToMissionChallenge(req.body, missionId) + ); + + res.status(StatusCodes.CREATED).json({ + result: userMission, + }); + } catch (error) { + next(error); + } +}; + +export const handleListStoreMissions = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const storeId = Number(req.params.storeId); + + const cursor = + typeof req.query.cursor === "string" + ? Number(req.query.cursor) + : 0; + + if (Number.isNaN(storeId)) { + throw new Error("storeId는 숫자여야 합니다."); + } + + if (Number.isNaN(cursor)) { + throw new Error("cursor는 숫자여야 합니다."); + } + + const missions = await listStoreMissions(storeId, cursor); + + res.status(StatusCodes.OK).json(missions); + } catch (error) { + next(error); + } +}; + +export const handleListInProgressMissions = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = Number(req.params.userId); + + const cursor = + typeof req.query.cursor === "string" + ? Number(req.query.cursor) + : 0; + + if (Number.isNaN(userId)) { + throw new Error("userId는 숫자여야 합니다."); + } + + if (Number.isNaN(cursor)) { + throw new Error("cursor는 숫자여야 합니다."); + } + + const missions = await listInProgressMissions(userId, cursor); + + res.status(StatusCodes.OK).json(missions); + } catch (error) { + next(error); + } +}; + +export const handleCompleteMission = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = Number(req.params.userId); + const missionId = Number(req.params.missionId); + + if (Number.isNaN(userId)) { + throw new Error("userId는 숫자여야 합니다."); + } + + if (Number.isNaN(missionId)) { + throw new Error("missionId는 숫자여야 합니다."); + } + + const result = await completeMission(userId, missionId); + + res.status(StatusCodes.OK).json({ + result, + }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/modules/missions/dtos/mission.dto.ts b/src/modules/missions/dtos/mission.dto.ts new file mode 100644 index 0000000..d71422d --- /dev/null +++ b/src/modules/missions/dtos/mission.dto.ts @@ -0,0 +1,108 @@ +export interface MissionCreateRequest { + title: string; + description: string; + rewardPoint: number; +} + +export const bodyToMission = ( + body: MissionCreateRequest, + storeId: number +) => { + return { + storeId, + title: body.title, + description: body.description, + rewardPoint: body.rewardPoint, + }; +}; + +export const responseFromMission = (mission: any) => { + return { + missionId: mission.id, + storeId: mission.store_id, + title: mission.title, + description: mission.description, + rewardPoint: mission.reward_point, + }; +}; + +export interface MissionChallengeRequest { + userId: number; +} + +export const bodyToMissionChallenge = ( + body: MissionChallengeRequest, + missionId: number +) => { + return { + userId: body.userId, + missionId, + }; +}; + +export const responseFromUserMission = (userMission: any) => { + return { + userMissionId: userMission.id, + userId: userMission.user_id, + missionId: userMission.mission_id, + status: userMission.status, + }; +}; + +export const responseFromStoreMissions = (missions: any[]) => { + const lastMission = missions[missions.length - 1]; + + return { + data: missions.map((mission) => ({ + missionId: mission.id, + title: mission.title, + description: mission.description, + rewardPoint: mission.rewardPoint, + createdAt: mission.createdAt, + store: { + storeId: mission.store.id, + name: mission.store.name, + address: mission.store.address, + }, + })), + pagination: { + cursor: lastMission ? lastMission.id : null, + }, + }; +}; +export const responseFromInProgressMissions = (userMissions: any[]) => { + const lastUserMission = userMissions[userMissions.length - 1]; + + return { + data: userMissions.map((userMission) => ({ + userMissionId: userMission.id, + status: userMission.status, + startedAt: userMission.startedAt, + mission: { + missionId: userMission.mission.id, + title: userMission.mission.title, + description: userMission.mission.description, + rewardPoint: userMission.mission.rewardPoint, + store: { + storeId: userMission.mission.store.id, + name: userMission.mission.store.name, + address: userMission.mission.store.address, + }, + }, + })), + pagination: { + cursor: lastUserMission ? lastUserMission.id : null, + }, + }; +}; + +export const responseFromCompletedMission = (userMission: any) => { + return { + userMissionId: userMission.id, + userId: userMission.userId, + missionId: userMission.missionId, + status: userMission.status, + startedAt: userMission.startedAt, + completedAt: userMission.completedAt, + }; +}; \ No newline at end of file diff --git a/src/modules/missions/repositories/mission.repository.ts b/src/modules/missions/repositories/mission.repository.ts new file mode 100644 index 0000000..e26f4f8 --- /dev/null +++ b/src/modules/missions/repositories/mission.repository.ts @@ -0,0 +1,175 @@ +import { prisma } from "../../../db.config.js"; + +export const findStoreById = async (storeId: number) => { + return await prisma.store.findFirst({ + where: { + id: storeId, + }, + }); +}; + +export const addMission = async (data: { + storeId: number; + title: string; + description: string; + rewardPoint: number; +}) => { + const createdMission = await prisma.mission.create({ + data: { + storeId: data.storeId, + title: data.title, + description: data.description, + rewardPoint: data.rewardPoint, + }, + }); + + return createdMission.id; +}; + +export const findMissionById = async (missionId: number) => { + return await prisma.mission.findFirst({ + where: { + id: missionId, + }, + }); +}; + +export const findUserById = async (userId: number) => { + return await prisma.user.findFirst({ + where: { + id: userId, + }, + }); +}; + +export const findUserMission = async ( + userId: number, + missionId: number +) => { + return await prisma.userMission.findFirst({ + where: { + userId, + missionId, + status: "IN_PROGRESS", + }, + }); +}; + +export const addUserMission = async (data: { + userId: number; + missionId: number; +}) => { + const createdUserMission = await prisma.userMission.create({ + data: { + userId: data.userId, + missionId: data.missionId, + status: "IN_PROGRESS", + }, + }); + + return createdUserMission.id; +}; + +export const findUserMissionById = async (userMissionId: number) => { + return await prisma.userMission.findFirst({ + where: { + id: userMissionId, + }, + }); +}; + +export const getMissionsByStoreId = async ( + storeId: number, + cursor: number +) => { + return await prisma.mission.findMany({ + where: { + storeId, + id: { + gt: cursor, + }, + }, + select: { + id: true, + title: true, + description: true, + rewardPoint: true, + createdAt: true, + store: { + select: { + id: true, + name: true, + address: true, + }, + }, + }, + orderBy: { + id: "asc", + }, + take: 5, + }); +}; +export const getInProgressMissionsByUserId = async ( + userId: number, + cursor: number +) => { + return await prisma.userMission.findMany({ + where: { + userId, + status: "IN_PROGRESS", + id: { + gt: cursor, + }, + }, + select: { + id: true, + status: true, + startedAt: true, + mission: { + select: { + id: true, + title: true, + description: true, + rewardPoint: true, + store: { + select: { + id: true, + name: true, + address: true, + }, + }, + }, + }, + }, + orderBy: { + id: "asc", + }, + take: 5, + }); +}; + +export const findInProgressUserMission = async ( + userId: number, + missionId: number +) => { + return await prisma.userMission.findFirst({ + where: { + userId, + missionId, + status: "IN_PROGRESS", + }, + }); +}; + +export const completeUserMission = async (userMissionId: number) => { + return await prisma.userMission.update({ + where: { + id: userMissionId, + }, + data: { + status: "COMPLETED", + completedAt: new Date(), + updatedAt: new Date(), + }, + }); +}; \ No newline at end of file diff --git a/src/modules/missions/services/mission.service.ts b/src/modules/missions/services/mission.service.ts new file mode 100644 index 0000000..39f9588 --- /dev/null +++ b/src/modules/missions/services/mission.service.ts @@ -0,0 +1,132 @@ +import { + addMission, + addUserMission, + findMissionById, + findStoreById, + findUserById, + findUserMission, + findUserMissionById, + getMissionsByStoreId, + getInProgressMissionsByUserId, + findInProgressUserMission, + completeUserMission, +} from "../repositories/mission.repository.js"; +import { + responseFromMission, + responseFromUserMission, + responseFromStoreMissions, + responseFromInProgressMissions, + responseFromCompletedMission, +} from "../dtos/mission.dto.js"; + +export const createMission = async (data: { + storeId: number; + title: string; + description: string; + rewardPoint: number; +}) => { + const store = await findStoreById(data.storeId); + + if (!store) { + throw new Error("존재하지 않는 가게입니다."); + } + + if (data.rewardPoint < 0) { + throw new Error("보상 포인트는 0 이상이어야 합니다."); + } + + const missionId = await addMission(data); + + const mission = await findMissionById(missionId); + + return responseFromMission(mission); +}; + +export const challengeMission = async (data: { + userId: number; + missionId: number; +}) => { + const user = await findUserById(data.userId); + + if (!user) { + throw new Error("존재하지 않는 사용자입니다."); + } + + const mission = await findMissionById(data.missionId); + + if (!mission) { + throw new Error("존재하지 않는 미션입니다."); + } + + const alreadyChallenged = await findUserMission( + data.userId, + data.missionId + ); + + if (alreadyChallenged) { + throw new Error("이미 도전 중인 미션입니다."); + } + + const userMissionId = await addUserMission(data); + + const userMission = await findUserMissionById(userMissionId); + + return responseFromUserMission(userMission); +}; + +export const listStoreMissions = async ( + storeId: number, + cursor: number +) => { + const store = await findStoreById(storeId); + + if (!store) { + throw new Error("존재하지 않는 가게입니다."); + } + + const missions = await getMissionsByStoreId(storeId, cursor); + + return responseFromStoreMissions(missions); +}; + +export const listInProgressMissions = async ( + userId: number, + cursor: number +) => { + const user = await findUserById(userId); + + if (!user) { + throw new Error("존재하지 않는 사용자입니다."); + } + + const missions = await getInProgressMissionsByUserId(userId, cursor); + + return responseFromInProgressMissions(missions); +}; + +export const completeMission = async ( + userId: number, + missionId: number +) => { + const user = await findUserById(userId); + + if (!user) { + throw new Error("존재하지 않는 사용자입니다."); + } + + const mission = await findMissionById(missionId); + + if (!mission) { + throw new Error("존재하지 않는 미션입니다."); + } + + const userMission = await findInProgressUserMission(userId, missionId); + + if (!userMission) { + throw new Error("진행 중인 미션이 아닙니다."); + } + + const completedMission = await completeUserMission(userMission.id); + + return responseFromCompletedMission(completedMission); +}; \ No newline at end of file diff --git a/src/modules/reviews/controllers/review.controller.ts b/src/modules/reviews/controllers/review.controller.ts new file mode 100644 index 0000000..66c4d34 --- /dev/null +++ b/src/modules/reviews/controllers/review.controller.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from "express"; +import { StatusCodes } from "http-status-codes"; +import { bodyToReview } from "../dtos/review.dto.js"; +import { + createReview, + listMyReviews, +} from "../services/review.service.js"; + +export const handleCreateReview = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + console.log("리뷰 추가를 요청했습니다."); + console.log("params:", req.params); + console.log("body:", req.body); + + const storeId = Number(req.params.storeId); + + if (Number.isNaN(storeId)) { + throw new Error("storeId는 숫자여야 합니다."); + } + + const review = await createReview(bodyToReview(req.body, storeId)); + + res.status(StatusCodes.CREATED).json({ + result: review, + }); + } catch (error) { + next(error); + } +}; + +export const handleListMyReviews = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const userId = Number(req.params.userId); + + const cursor = + typeof req.query.cursor === "string" + ? Number(req.query.cursor) + : 0; + + if (Number.isNaN(userId)) { + throw new Error("userId는 숫자여야 합니다."); + } + + if (Number.isNaN(cursor)) { + throw new Error("cursor는 숫자여야 합니다."); + } + + const reviews = await listMyReviews(userId, cursor); + + res.status(StatusCodes.OK).json(reviews); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/modules/reviews/dtos/review.dto.ts b/src/modules/reviews/dtos/review.dto.ts new file mode 100644 index 0000000..7511f35 --- /dev/null +++ b/src/modules/reviews/dtos/review.dto.ts @@ -0,0 +1,48 @@ +export interface ReviewCreateRequest { + rating: number; + content: string; +} + +export const bodyToReview = ( + body: ReviewCreateRequest, + storeId: number +) => { + return { + storeId, + rating: body.rating, + content: body.content, + }; +}; + +export const responseFromReview = (review: any) => { + return { + reviewId: review.id, + storeId: review.store_id, + rating: review.rating, + content: review.content, + }; +}; + +export const responseFromMyReviews = (reviews: any[]) => { + const lastReview = reviews[reviews.length - 1]; + + return { + data: reviews.map((review) => ({ + reviewId: review.id, + rating: review.rating, + content: review.content, + createdAt: review.createdAt, + store: { + storeId: review.store.id, + name: review.store.name, + address: review.store.address, + }, + })), + + pagination: { + // 다음 요청에서 사용할 cursor + // 리뷰가 없으면 null 반환 + cursor: lastReview ? lastReview.id : null, + }, + }; +}; \ No newline at end of file diff --git a/src/modules/reviews/repositories/review.repository.ts b/src/modules/reviews/repositories/review.repository.ts new file mode 100644 index 0000000..c887e87 --- /dev/null +++ b/src/modules/reviews/repositories/review.repository.ts @@ -0,0 +1,79 @@ +import { prisma } from "../../../db.config.js"; + +export const findStoreById = async (storeId: number) => { + return await prisma.store.findFirst({ + where: { + id: storeId, + }, + }); +}; + +export const addReview = async (data: { + storeId: number; + rating: number; + content: string; + userId?: number; +}) => { + const createdReview = await prisma.review.create({ + data: { + storeId: data.storeId, + + // 기존 5주차 리뷰 API는 userId를 받지 않았기 때문에 + // 임시로 userId가 없으면 1번 사용자를 사용하도록 처리 + userId: data.userId ?? 1, + + rating: data.rating, + content: data.content, + }, + }); + + return createdReview.id; +}; + +export const findReviewById = async (reviewId: number) => { + return await prisma.review.findFirst({ + where: { + id: reviewId, + }, + }); +}; + +export const getReviewsByUserId = async ( + userId: number, + cursor: number +) => { + return await prisma.review.findMany({ + where: { + userId, + + // cursor보다 큰 id를 가진 리뷰만 조회 + id: { + gt: cursor, + }, + }, + + select: { + id: true, + rating: true, + content: true, + createdAt: true, + + // 리뷰가 작성된 가게 정보도 함께 가져옴 + store: { + select: { + id: true, + name: true, + address: true, + }, + }, + }, + + // id가 작은 순서대로 조회 + orderBy: { + id: "asc", + }, + + // 한 번에 5개만 조회 + take: 5, + }); +}; \ No newline at end of file diff --git a/src/modules/reviews/services/review.service.ts b/src/modules/reviews/services/review.service.ts new file mode 100644 index 0000000..0dd21b6 --- /dev/null +++ b/src/modules/reviews/services/review.service.ts @@ -0,0 +1,42 @@ +import { + addReview, + findReviewById, + findStoreById, + getReviewsByUserId, +} from "../repositories/review.repository.js"; + +import { + responseFromReview, + responseFromMyReviews, +} from "../dtos/review.dto.js"; + +export const createReview = async (data: { + storeId: number; + rating: number; + content: string; +}) => { + const store = await findStoreById(data.storeId); + + if (!store) { + throw new Error("존재하지 않는 가게입니다."); + } + + if (data.rating < 1 || data.rating > 5) { + throw new Error("평점은 1점부터 5점 사이여야 합니다."); + } + + const reviewId = await addReview(data); + + const review = await findReviewById(reviewId); + + return responseFromReview(review); +}; + +export const listMyReviews = async ( + userId: number, + cursor: number +) => { + const reviews = await getReviewsByUserId(userId, cursor); + + return responseFromMyReviews(reviews); +}; \ No newline at end of file diff --git a/src/modules/stores/controllers/store.controller.ts b/src/modules/stores/controllers/store.controller.ts new file mode 100644 index 0000000..75e93e4 --- /dev/null +++ b/src/modules/stores/controllers/store.controller.ts @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from "express"; +import { StatusCodes } from "http-status-codes"; +import { bodyToStore } from "../dtos/store.dto.js"; +import { createStore } from "../services/store.service.js"; + +export const handleCreateStore = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + console.log("가게 추가를 요청했습니다."); + console.log("params:", req.params); + console.log("body:", req.body); + + const regionId = Number(req.params.regionId); + + if (Number.isNaN(regionId)) { + throw new Error("regionId는 숫자여야 합니다."); + } + + const store = await createStore(bodyToStore(req.body, regionId)); + + res.status(StatusCodes.CREATED).json({ + result: store, + }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/src/modules/stores/dtos/store.dto.ts b/src/modules/stores/dtos/store.dto.ts new file mode 100644 index 0000000..236cc3c --- /dev/null +++ b/src/modules/stores/dtos/store.dto.ts @@ -0,0 +1,21 @@ +export interface StoreCreateRequest { + name: string; + address: string; +} + +export const bodyToStore = (body: StoreCreateRequest, regionId: number) => { + return { + regionId, + name: body.name, + address: body.address, + }; +}; + +export const responseFromStore = (store: any) => { + return { + storeId: store.id, + regionId: store.region_id, + name: store.name, + address: store.address, + }; +}; \ No newline at end of file diff --git a/src/modules/stores/repositories/store.repository.ts b/src/modules/stores/repositories/store.repository.ts new file mode 100644 index 0000000..5b30705 --- /dev/null +++ b/src/modules/stores/repositories/store.repository.ts @@ -0,0 +1,33 @@ +import { prisma } from "../../../db.config.js"; + +export const findRegionById = async (regionId: number) => { + return await prisma.region.findFirst({ + where: { + id: regionId, + }, + }); +}; + +export const addStore = async (data: { + regionId: number; + name: string; + address: string; +}) => { + const createdStore = await prisma.store.create({ + data: { + regionId: data.regionId, + name: data.name, + address: data.address, + }, + }); + + return createdStore.id; +}; + +export const findStoreById = async (storeId: number) => { + return await prisma.store.findFirst({ + where: { + id: storeId, + }, + }); +}; \ No newline at end of file diff --git a/src/modules/stores/services/store.service.ts b/src/modules/stores/services/store.service.ts new file mode 100644 index 0000000..7087e8d --- /dev/null +++ b/src/modules/stores/services/store.service.ts @@ -0,0 +1,24 @@ +import { + addStore, + findRegionById, + findStoreById, +} from "../repositories/store.repository.js"; +import { responseFromStore } from "../dtos/store.dto.js"; + +export const createStore = async (data: { + regionId: number; + name: string; + address: string; +}) => { + const region = await findRegionById(data.regionId); + + if (!region) { + throw new Error("존재하지 않는 지역입니다."); + } + + const storeId = await addStore(data); + + const store = await findStoreById(storeId); + + return responseFromStore(store); +}; \ No newline at end of file diff --git a/src/modules/users/controllers/user.controller.ts b/src/modules/users/controllers/user.controller.ts new file mode 100644 index 0000000..2010fc8 --- /dev/null +++ b/src/modules/users/controllers/user.controller.ts @@ -0,0 +1,94 @@ +import { + Body, + Controller, + Get, + Middlewares, + Post, + Request, + Route, + Tags, +} from "tsoa"; +import { Request as ExpressRequest } from "express"; + +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { userSignUp } from "../services/user.service.js"; +import { ApiResponse, success } from "../../../common/responses/response.js"; +import { authorizeUser } from "../../../common/middlewares/auth.middleware.js"; + +@Route("users") +@Tags("Users") +export class UserController extends Controller { + @Post("signup") + public async handleUserSignUp( + @Body() body: UserSignUpRequest + ): Promise> { + console.log("회원가입을 요청했습니다!"); + console.log("body:", body); + + const user = await userSignUp(body); + + return success(user); + } + + @Get("guest") + public async handleGuestPage(): Promise { + return ` +

게스트 페이지

+

이 페이지는 로그인이 필요 없습니다.

+ + `; + } + + @Get("login") + public async handleLoginPage(): Promise { + return ` +

로그인 페이지

+

로그인이 필요한 페이지에서 이동합니다.

+ 로그인 쿠키 생성하기 + `; + } + + @Get("mypage") + @Middlewares(authorizeUser()) + public async handleMypage( + @Request() req: ExpressRequest + ): Promise { + return ` +

마이페이지

+

환영합니다, ${req.cookies.username}님!

+

이 페이지는 로그인한 사람만 볼 수 있습니다.

+ 로그아웃 + `; + } + + @Get("set-login") + public async handleSetLogin( + @Request() req: ExpressRequest + ): Promise { + req.res!.cookie("username", "UMC10th", { + maxAge: 3600000, + httpOnly: true, + }); + + return ` +

로그인 쿠키 생성 완료

+

username=UMC10th 쿠키가 생성되었습니다.

+ 마이페이지로 이동 + `; + } + + @Get("set-logout") + public async handleSetLogout( + @Request() req: ExpressRequest + ): Promise { + req.res!.clearCookie("username"); + + return ` +

로그아웃 완료

+

쿠키가 삭제되었습니다.

+ 게스트 페이지로 이동 + `; + } +} \ No newline at end of file diff --git a/src/modules/users/dtos/user.dto.ts b/src/modules/users/dtos/user.dto.ts new file mode 100644 index 0000000..6a75302 --- /dev/null +++ b/src/modules/users/dtos/user.dto.ts @@ -0,0 +1,20 @@ +export interface UserSignUpRequest { + email: string; + password?: string; + name: string; + gender: string; + + // JSON 요청에서는 날짜가 문자열로 들어오므로 string으로 받는 것이 안전합니다. + // Service에서 new Date(data.birth)로 Date 객체로 변환합니다. + birth: string; + + address?: string; + detailAddress?: string; + phoneNumber: string; + preferences: number[]; +} + +export interface UserSignUpResponse { + userId: number; + preferences: string[]; +} \ No newline at end of file diff --git a/src/modules/users/repositories/user.repository.ts b/src/modules/users/repositories/user.repository.ts new file mode 100644 index 0000000..1c77d44 --- /dev/null +++ b/src/modules/users/repositories/user.repository.ts @@ -0,0 +1,73 @@ +import { prisma } from "../../../db.config.js"; + +export const addUser = async (data: { + email: string; + password?: string; + name: string; + gender: string; + birth: Date; + address: string; + detailAddress: string; + phoneNumber: string; +}) => { + // 1. 이미 존재하는 이메일인지 확인 + const user = await prisma.user.findFirst({ + where: { + email: data.email, + }, + }); + + if (user) { + return null; + } + + // 2. 새로운 사용자 생성 + const createdUser = await prisma.user.create({ + data: { + email: data.email, + password: data.password, + name: data.name, + gender: data.gender, + birth: data.birth, + address: data.address, + detailAddress: data.detailAddress, + phoneNumber: data.phoneNumber, + }, + }); + + return createdUser.id; +}; + +export const setPreference = async ( + userId: number, + foodCategoryId: number +) => { + await prisma.userFavorCategory.create({ + data: { + userId, + foodCategoryId, + }, + }); +}; + +export const getUser = async (userId: number) => { + return await prisma.user.findFirstOrThrow({ + where: { + id: userId, + }, + }); +}; + +export const getUserPreferencesByUserId = async (userId: number) => { + return await prisma.userFavorCategory.findMany({ + where: { + userId, + }, + include: { + foodCategory: true, + }, + orderBy: { + foodCategoryId: "asc", + }, + }); +}; \ No newline at end of file diff --git a/src/modules/users/services/user.service.ts b/src/modules/users/services/user.service.ts new file mode 100644 index 0000000..0abd917 --- /dev/null +++ b/src/modules/users/services/user.service.ts @@ -0,0 +1,51 @@ +import { UserSignUpRequest, UserSignUpResponse } from "../dtos/user.dto.js"; +import { + addUser, + getUser, + getUserPreferencesByUserId, + setPreference, +} from "../repositories/user.repository.js"; +import { DuplicateUserEmailError } from "../../../common/errors/user.error.js"; + +export const userSignUp = async ( + data: UserSignUpRequest +): Promise => { + const joinUserId = await addUser({ + email: data.email, + password: data.password, + name: data.name, + gender: data.gender, + + // 요청 body에서는 birth가 문자열로 들어오기 때문에 Date 객체로 변환합니다. + birth: new Date(data.birth), + + // 선택값은 undefined로 들어올 수 있으므로 기본값을 넣어줍니다. + address: data.address ?? "", + detailAddress: data.detailAddress ?? "", + phoneNumber: data.phoneNumber, + }); + + // addUser에서 이메일 중복이면 null을 반환하도록 되어 있으므로, + // 이 경우 커스텀 에러를 던집니다. + if (joinUserId === null) { + throw new DuplicateUserEmailError(data); + } + + // 사용자가 선택한 음식 선호 카테고리를 저장합니다. + for (const preference of data.preferences) { + await setPreference(joinUserId, preference); + } + + // 생성된 사용자 정보를 다시 조회합니다. + const user = await getUser(joinUserId); + + // 사용자의 선호 카테고리 이름 목록을 조회합니다. + const preferences = (await getUserPreferencesByUserId(joinUserId)).map( + (obj) => obj.foodCategory.name + ); + + return { + userId: user.id, + preferences, + }; +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f8884ca..90d2f93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,11 +11,11 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "libReplacement": true, /* Enable lib replacement. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ @@ -26,9 +26,9 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, + "module": "CommonJS" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ "rootDir": "src" /* Allow multiple folders to be treated as one when resolving modules. */, diff --git a/tsoa.json b/tsoa.json new file mode 100644 index 0000000..804be4b --- /dev/null +++ b/tsoa.json @@ -0,0 +1,12 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*.controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3 + }, + "routes": { + "routesDir": "src/generated" + } +} \ No newline at end of file