Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
3145902
chore: dev 환경에서 프록시 테스트를 위해 http-proxy-middleware 패키지 설치
dioo1461 Mar 26, 2025
94b6336
feat: discussionId 인코딩 및 디코딩을 위한 apiMiddleware 모듈 작성
dioo1461 Mar 26, 2025
abc43cc
feat: response body 내에 discussionId 필드가 있을 경우 encoding하는 로직 작성
dioo1461 Mar 26, 2025
0f1e4ef
feat: 인코딩된 discussionId를 처리하기 위해 Zod로 정의한 discussionId 타입을 string으로 통일
dioo1461 Mar 26, 2025
13746d3
fix: login 요청에 대해 401 에러를 반환하던 문제 수정
dioo1461 Mar 26, 2025
640052f
fix: path 변경에 따라 discussionId url 디코딩 로직 수정
dioo1461 Mar 26, 2025
971923c
fix: 일부 number type으로 남아있던 discussionId 타입을 모두 string으로 변경
dioo1461 Mar 27, 2025
b885cf1
feat: url-safe base64 적용
dioo1461 Mar 27, 2025
d559d20
fix: proxy를 거치는 모든 응답이 statusCode 200으로 전달되던 문제 수정
dioo1461 Mar 27, 2025
9103920
chore: discussionId 타입 string으로 변경
dioo1461 Mar 27, 2025
32d3cba
refactor: 디렉토리 레이어 분리
dioo1461 Mar 27, 2025
1cc053a
refactor: base64 인코딩 및 디코딩 로직을 util 레이어로 분리
dioo1461 Mar 27, 2025
c903cc4
chore: 잘못된 import 문법 수정
dioo1461 Mar 27, 2025
6439af0
chore: vite 외부에서 env를 사용하기 위해 dotenv 패키지 설치
dioo1461 Mar 27, 2025
8194048
chore: 서버 측 env 세팅
dioo1461 Mar 27, 2025
5bd47d6
fix: env가 로드되지 않던 문제 수정
dioo1461 Mar 27, 2025
978fadb
feat: 암복호화 알고리즘을 base64 -> aes-gcm으로 교체
dioo1461 Mar 27, 2025
6d15865
fix: 요청 payload를 base64로 인코딩하고 있던 문제 수정
dioo1461 Mar 27, 2025
6482843
refactor: 책임과 역할에 따라 함수들의 레이어 분리
dioo1461 Mar 27, 2025
533c0f1
chore: typo 등 수정
dioo1461 Mar 27, 2025
32d223e
fix: 빌드 에러 해결
dioo1461 Jun 13, 2025
0c3ed1d
chore: 누락된 파일들 server 패키지로 이동
dioo1461 Jun 13, 2025
19e24e6
refactor: js 코드를 ts로 마이그레이션
dioo1461 Jun 13, 2025
2105f2c
chore: gitignore에 .env 추가
dioo1461 Jun 13, 2025
9f86875
feat: express 인스턴스에 미들웨어 등록
dioo1461 Jun 13, 2025
d2b9f44
chore: 남아 있던 레거시 파일 삭제
dioo1461 Jun 13, 2025
46a012f
chore: http-proxy-middleware 패키지를 devdependency -> dependency 로 이전
dioo1461 Jun 13, 2025
bd63602
fix: 리프레시 로직이 동작하지 않던 문제 수정
dioo1461 Jun 15, 2025
05ff8a2
chore: pnpm-lock 업데이트
dioo1461 Jun 15, 2025
5fec840
refactor: 의미 명확화를 위해 `url` -> `path`로 변수명 변경
dioo1461 Jul 13, 2025
0be4ed1
refactor: 코드 리뷰 반영 - 코드 가독성 개선, 휴먼에러 수정
dioo1461 Jul 13, 2025
1f423ea
fix: 빌드 에러 해결 및 불필요한 의존성 제거
dioo1461 Jul 13, 2025
02fcae2
refactor: 폴더 구조 변경
dioo1461 Jul 13, 2025
f7bc78d
feat: 런타임 환경 체크하는 구문을 함수로 분리, dotenv 또한 개발환경에서만 동작하도록 구현 변경
dioo1461 Jul 13, 2025
d85c283
refactor: 하드코딩된 url origin을 env에서 관리하도록 수정
dioo1461 Jul 13, 2025
1ca97f9
fix: 빌드 시 index.js가 참조하는 모든 모듈이 번들에서 제외되던 문제 수정
dioo1461 Jul 13, 2025
35052d3
chore: remove misleading comment
dioo1461 Jul 13, 2025
2d2c58e
feat: 요청이 404 에러 반환 시 `/not-found`로 리다이렉트하도록 구현
dioo1461 Jul 25, 2025
8f78da1
fix: 암호화된 discussionId 복호화 실패 시 던져지는 에러를 처리
dioo1461 Jul 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
.env

# Editor directories and files
.vscode/*
Expand Down
10 changes: 4 additions & 6 deletions frontend/apps/client/src/features/discussion/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import type {
DiscussionCalendarRequest,
DiscussionCalendarResponse,
DiscussionConfirmRequest,
DiscussionParticipantResponse,
DiscussionConfirmResponse, DiscussionParticipantResponse,
DiscussionRankRequest,
DiscussionRankResponse,
DiscussionRequest,
DiscussionResponse } from '../model';
import {
DiscussionConfirmResponse,
DiscussionResponse,
} from '../model';

export const discussionApi = {
Expand Down Expand Up @@ -64,11 +62,11 @@ export const candidateApi = {
{ id, body }: { id: string; body: DiscussionConfirmRequest },
): Promise<DiscussionConfirmResponse> => {
const response = await request.post(`/api/v1/discussion/${id}/confirm`, { body });
return DiscussionConfirmResponse.parse(response);
return response.parse(response);
},

getDiscussionConfirm: async (id: string): Promise<DiscussionConfirmResponse> => {
const response = await request.get(`/api/v1/discussion/${id}/shared-event`);
return DiscussionConfirmResponse.parse(response);
return response.parse(response);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { request } from '@utils/fetch';
import { InvitationJoinResponseSchema, InvitationResponseSchema } from '../model/invitation';

export const invitationApi = {
getInvitationInfo: async (discussionId: number) => {
getInvitationInfo: async (discussionId: string) => {
const response = await request.get(`/api/v1/discussion/${discussionId}/invite`);
const validData = InvitationResponseSchema.parse(response);
return validData;
},
postInviatationJoin: async (discussionId: number, password?: string) => {
postInviatationJoin: async (discussionId: string, password?: string) => {
const response = await request.post(`/api/v1/discussion/${discussionId}/join`, {
body: { password: password },
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/apps/client/src/features/discussion/api/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const participantKeys = {
detail: (id: string) => [...participantKeys.all, id],
};

export const invitationQueryKey = (discussionId: number) => ['invitation', discussionId];
export const invitationQueryKey = (discussionId: string) => ['invitation', discussionId];

export const sharedEventKeys = {
all: ['sharedEvents'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const useDiscussionMutation = () => {
export const useInvitationJoinMutation = () => {
const { mutate } = useMutation({
mutationFn: ({ body }: {
body: { discussionId: number; password?: string };
body: { discussionId: string; password?: string };
}) => invitationApi.postInviatationJoin(body.discussionId, body.password),
});
return { mutate };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,5 @@ export const useDiscussionHostQuery = (discussionId: string) => {

return { isHost, isPending };
};
export const useInviteInfoQuery = (discussionId: number) =>
export const useInviteInfoQuery = (discussionId: string) =>
useQuery<InviteResponse>(invitationQueryOption(discussionId));
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { candidateApi } from '.';
import { invitationApi } from './invitationApi';
import { candidateKeys, invitationQueryKey } from './keys';

export const invitationQueryOption = (discussionId: number) => ({
export const invitationQueryOption = (discussionId: string) => ({
queryKey: invitationQueryKey(discussionId),
queryFn: () => invitationApi.getInvitationInfo(discussionId),
});
Expand Down
6 changes: 3 additions & 3 deletions frontend/apps/client/src/features/discussion/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const DiscussionRequest = z.object({
const DiscussionConfirmRequest = SharedEventDTO.omit({ id: true });

const DiscussionResponse = z.object({
id: z.number(),
id: z.string(),
title: z.string(),
dateRangeStart: z.string().regex(DATE_BAR),
dateRangeEnd: z.string().regex(DATE_BAR),
Expand Down Expand Up @@ -77,8 +77,8 @@ const DiscussionRankResponse = z.object({
eventsRankedOfTime: z.array(DiscussionDTO),
});

export const DiscussionConfirmResponse = z.object({
discussionId: z.number(),
const DiscussionConfirmResponse = z.object({
discussionId: z.string(),
title: z.string(),
meetingMethodOrLocation: z.string().nullable(),
sharedEventDto: SharedEventDTO.transform((event) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { inputStyle } from './index.css';
import TimerButton from './TimerButton';

interface SubmitFormProps {
discussionId: number;
discussionId: string;
canJoin: boolean;
requirePW: boolean;
unlockDateTime: Date | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import SubmitForm from './SubmitForm';

export interface DiscussionInviteCardProps {
discussionId: number;
discussionId: string;
hostName: string;
title: string;
canJoin: boolean;
Expand Down Expand Up @@ -61,7 +61,7 @@ const DiscussionInviteCardContents = (props: DiscussionInviteCardContentsProps)

interface DiscussionInviteCardFooterProps {
canJoin: boolean;
discussionId: number;
discussionId: string;
requirePassword: boolean;
timeUnlocked: Date | null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from 'zod';
import { SharedEventDtoSchema } from './SharedEventDto';

const FinishedScheduleSchema = z.object({
discussionId: z.number(),
discussionId: z.string(),
title: z.string(),
meetingMethodOrLocation: z.union([z.string(), z.null()]),
sharedEventDto: z.union([SharedEventDtoSchema, z.null()]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { zCoerceToDate } from '@utils/zod';
import { z } from 'zod';

const OngoingScheduleSchema = z.object({
discussionId: z.number(),
discussionId: z.string(),
title: z.string(),
dateRangeStart: zCoerceToDate,
dateRangeEnd: zCoerceToDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ParticipantWithEventsSchema } from '@/features/timeline-schedule/model'
import { SharedEventDtoSchema } from './SharedEventDto';

export const UpcomingScheduleSchema = z.object({
discussionId: z.number(),
discussionId: z.string(),
title: z.string(),
meetingMethodOrLocation: z.union([z.string(), z.null()]),
sharedEventDto: SharedEventDtoSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const RecommendedScheduleItem = ({
candidate, discussionId, startDTStr, endDTStr, adjustCount,
}: {
candidate: DiscussionDTO;
discussionId: number;
discussionId: string;
startDTStr: string;
endDTStr: string;
adjustCount: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from './ScheduleDetails.css';

interface ScheduleDetailsProps {
discussionId: number;
discussionId: string;
}

// TODO: Date 타입 변환 후 변경사항 적용
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const CandidateDetailRequestSchema = z.object({
});

export const CandidateDetailResponseSchema = z.object({
discussionId: z.number(),
discussionId: z.string(),
startDateTime: zCoerceToDate,
endDateTime: zCoerceToDate,
participants: z.array(ParticipantWithEventsSchema),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const DiscussionInvitePage = ({ invitation }: { invitation: InviteResponse }) =>
<DiscussionInviteCard
canJoin={!isFull}
dateRange={{ start: dateRangeStart, end: dateRangeEnd }}
discussionId={+id}
discussionId={id}
hostName={host}
meetingDuration={duration}
requirePassword={requirePassword}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { type DateRangeReturn, useSelectDateRange } from '@hooks/useSelectDateRa
import type { Dispatch, SetStateAction } from 'react';

interface DiscussionContextProps {
selectedId: number | null;
setSelectedId: Dispatch<SetStateAction<number | null>>;
selectedId: string | null;
setSelectedId: Dispatch<SetStateAction<string | null>>;
}

export const {
Expand Down
2 changes: 1 addition & 1 deletion frontend/apps/client/src/pages/MyCalendarPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { TableProvider } from './TableContext';

const MyCalendarPage = () => {
const [selectedId, setSelectedId] = useState<number | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);

return (
<div className={containerStyle}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const DiscussionInvite = () => {

export const Route = createFileRoute('/_main/discussion/invite/$id')({
loader: async ({ params: { id }, context }) => {
const invitation = await context.queryClient.fetchQuery(invitationQueryOption(Number(id)));
const invitation = await context.queryClient.fetchQuery(invitationQueryOption(id));
return { invitation };
},
component: DiscussionInvite,
Expand Down
1 change: 1 addition & 0 deletions frontend/apps/client/src/utils/error/HTTPError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class HTTPError extends Error {

isUnAuthorizedError = () => this.#status === 401;
isForbiddenError = () => this.#status === 403;
isNotFoundError = () => this.#status === 404;
isTooManyRequestsError = () => this.#status === 429;

isInvalidRefreshTokenError = () => this.#code.startsWith('RT');
Expand Down
3 changes: 2 additions & 1 deletion frontend/apps/client/src/utils/error/handleError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export const handleError = (error: unknown) => {
queueMicrotask(() => {
addNoti({ type: 'error', title: error.message });
});

if (error.isUnAuthorizedError()) window.location.href = '/login';
if (error.isForbiddenError()) window.location.href = '/landing';
if (error.isNotFoundError()) window.location.href = '/not-found';
return false;
}

if (error instanceof Error) {

queueMicrotask(() => {
addNoti({ type: 'error', title: error.message });
});
Expand Down
6 changes: 3 additions & 3 deletions frontend/apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"start": "cross-env NODE_ENV=start tsup --watch"
},
"dependencies": {
"express": "^5.1.0"
"express": "^5.1.0",
"http-proxy-middleware": "^3.0.3"
},
"devDependencies": {
"@types/express": "^5.0.2",
"typescript": "^5.6.2",
"@endolphin/tsup-config": "workspace:^",
"@endolphin/vitest-config": "workspace:^"
"@endolphin/tsup-config": "workspace:^"
}
}
18 changes: 18 additions & 0 deletions frontend/apps/server/src/envconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import process from 'node:process';
import { fileURLToPath } from 'node:url';

import path from 'path';

import { isProduction } from './utils/isProduction';

const dirname = path.dirname(fileURLToPath(import.meta.url));

if (!isProduction) {
const dotenv = await import('dotenv');
dotenv.config({ path: path.resolve(dirname, '../.env') });
}

export const serverEnv = {
AES_GCM_KEY_OF_DISCUSSION_ID: process.env.AES_GCM_KEY_OF_DISCUSSION_ID,
BASE_URL: process.env.BASE_URL,
};
11 changes: 9 additions & 2 deletions frontend/apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import { fileURLToPath } from 'node:url';
import express from 'express';
import type { ViteDevServer } from 'vite';

const isProduction = process.env.NODE_ENV === 'production';
import { discussionIdTransformer, discussionIdVerifier } from './middlewares/discussionId';
import { isProduction } from './utils/isProduction';

const port = process.env.PORT || 5173;
const base = process.env.BASE || '/';

Expand Down Expand Up @@ -69,9 +71,14 @@ const serveHTML = async () => {
};

const createServer = async () => {
// 응답 body를 JSON으로 파싱 (middleware 내부에서 req.body 형태로 사용)
app.use(express.json());
// discussion id를 인코딩 및 디코딩하는 모듈 적용
app.use('/api', discussionIdVerifier);
app.use('/api', discussionIdTransformer);
await addMiddleware();
await serveHTML();

if (!isProduction) {
const httpsOptions = {
key: fs.readFileSync(keypath),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Buffer } from 'node:buffer';

import { serverEnv } from '../../envconfig';
import { aesGcm } from '../../utils/cipher';
import { base64Url } from '../../utils/stringTransformer';

const aesGcmKey = serverEnv.AES_GCM_KEY_OF_DISCUSSION_ID;
if (!aesGcmKey) {
throw new Error('환경 변수 AES_GCM_KEY_OF_DISCUSSION_ID가 정의되지 않았습니다.');
}

export const decodeDiscussionIdOfUrl = (path: string) => {
// (출력 예시) parts: ['', 'v1', 'discussion', '{encoded_discussion_id}', 'invite']
const parts = path.split('/');
const targetIdx = 3; // discussionId가 위치하는 index
if (parts.length > targetIdx) {
parts[targetIdx] = decryptDiscussionId(parts[targetIdx]);
}
return parts.join('/');
};
Comment on lines +12 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider making the URL structure more flexible and add validation.

The current implementation assumes a fixed URL structure with the discussion ID always at index 3. This could be fragile if the API structure changes.

export const decodeDiscussionIdOfUrl = (path: string) => {
  const parts = path.split('/'); 
-  const targetIdx = 3; // discussionId가 위치하는 index
-  if (parts.length > targetIdx) {
-    parts[targetIdx] = decryptDiscussionId(parts[targetIdx]);
-  }
+  const targetIdx = 3; // discussionId가 위치하는 index
+  if (parts.length <= targetIdx) {
+    throw new Error('Invalid URL structure for discussion ID extraction');
+  }
+  if (!parts[targetIdx] || parts[targetIdx].trim() === '') {
+    throw new Error('Discussion ID not found in expected URL position');
+  }
+  parts[targetIdx] = decryptDiscussionId(parts[targetIdx]);
  return parts.join('/');
};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const decodeDiscussionIdOfUrl = (path: string) => {
// (출력 예시) parts: ['', 'v1', 'discussion', '{encoded_discussion_id}', 'invite']
const parts = path.split('/');
const targetIdx = 3; // discussionId가 위치하는 index
if (parts.length > targetIdx) {
parts[targetIdx] = decryptDiscussionId(parts[targetIdx]);
}
return parts.join('/');
};
export const decodeDiscussionIdOfUrl = (path: string) => {
// (출력 예시) parts: ['', 'v1', 'discussion', '{encoded_discussion_id}', 'invite']
const parts = path.split('/');
const targetIdx = 3; // discussionId가 위치하는 index
if (parts.length <= targetIdx) {
throw new Error('Invalid URL structure for discussion ID extraction');
}
if (!parts[targetIdx] || parts[targetIdx].trim() === '') {
throw new Error('Discussion ID not found in expected URL position');
}
parts[targetIdx] = decryptDiscussionId(parts[targetIdx]);
return parts.join('/');
};
🤖 Prompt for AI Agents
In frontend/apps/server/src/middlewares/discussionId/discussionCipher.ts around
lines 12 to 20, the code assumes the discussion ID is always at index 3 in the
URL path, which is fragile. Refactor the function to dynamically locate the
discussion ID segment by searching for the 'discussion' keyword in the path
parts array, then decrypt the following segment if it exists. Also, add
validation to ensure the expected segments exist before attempting decryption to
avoid errors with unexpected URL structures.


export const encryptDiscussionId = (text: string) => {
const key = Buffer.from(aesGcmKey, 'base64');
const { iv, content, tag } = aesGcm.encrypt(text, key);
const concat = `${iv}.${content}.${tag}`;
const cipherText = base64Url.encode(concat);
return cipherText;
};

export const decryptDiscussionId = (cipherText: string) => {
const key = Buffer.from(aesGcmKey, 'base64');
const concat = base64Url.decode(cipherText);
const [iv, content, tag] = concat.split('.');
const encryptedData = { iv, content, tag };
return aesGcm.decrypt(encryptedData, key);
};
Comment on lines +30 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add proper error handling and input validation for decryption.

The decryption function needs better error handling and input validation to prevent crashes and provide meaningful error messages.

export const decryptDiscussionId = (cipherText: string) => {
+  if (!cipherText || cipherText.trim() === '') {
+    throw new Error('Cipher text cannot be empty');
+  }
+  
+  try {
    const key = Buffer.from(aesGcmKey, 'base64');
    const concat = base64Url.decode(cipherText);
    const [iv, content, tag] = concat.split('.');
+    
+    if (!iv || !content || !tag) {
+      throw new Error('Invalid cipher text format');
+    }
+    
    const encryptedData = { iv, content, tag };
    return aesGcm.decrypt(encryptedData, key);
+  } catch (error) {
+    if (error instanceof Error) {
+      throw new Error(`Failed to decrypt discussion ID: ${error.message}`);
+    }
+    throw new Error('Failed to decrypt discussion ID: Unknown error');
+  }
};
🤖 Prompt for AI Agents
In frontend/apps/server/src/middlewares/discussionId/discussionCipher.ts around
lines 30 to 36, the decryptDiscussionId function lacks input validation and
error handling. Add checks to ensure the cipherText is a valid string and
properly formatted before processing. Wrap the decryption logic in a try-catch
block to catch and handle any errors, returning a clear error message or
handling the failure gracefully instead of allowing the function to crash.

35 changes: 35 additions & 0 deletions frontend/apps/server/src/middlewares/discussionId/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { RequestHandler } from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

import { serverEnv } from '../../envconfig';
import { decodeDiscussionIdOfUrl } from './discussionCipher';
import {
DISCUSSION_CREATE_ENDPOINT,
handleProxyReq,
handleProxyRes,
rewriteDiscussionPath,
} from './proxyHandlers';

export const discussionIdTransformer = createProxyMiddleware({
target: `${serverEnv.BASE_URL}/api`,
changeOrigin: true,
selfHandleResponse: true,
pathRewrite: rewriteDiscussionPath,
on: {
proxyReq: handleProxyReq,
proxyRes: handleProxyRes,
},
});
Comment on lines +13 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify BASE_URL configuration and consider security implications.

The proxy middleware configuration looks correct, but there are a few considerations:

  1. The selfHandleResponse: true setting is appropriate since you're manually handling responses for encryption
  2. Consider adding request/response size limits to prevent potential DoS attacks
  3. Verify that serverEnv.BASE_URL is properly validated and doesn't allow malicious redirects

🏁 Script executed:

#!/bin/bash
# Check if BASE_URL validation exists in envconfig
rg -A 5 -B 5 "BASE_URL.*validation|validate.*BASE_URL" frontend/apps/server/src/

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Locate and inspect the serverEnv definition to verify BASE_URL validation
file=$(rg -l "export const serverEnv" -R frontend/apps/server/src | head -n 1)
echo "Found serverEnv in: $file"
sed -n '1,200p' "$file"

Length of output: 601


🏁 Script executed:

#!/bin/bash
# Inspect imports in discussionIdTransformer to locate serverEnv source
sed -n '1,50p' frontend/apps/server/src/middlewares/discussionId/index.ts

Length of output: 1034


🏁 Script executed:

#!/bin/bash
# Locate envconfig file defining serverEnv
env_file=$(fd 'envconfig\.(ts|js)$' frontend/apps/server/src)
echo "Found envconfig file(s):"
echo "$env_file"
# Display the definition of serverEnv including any validation logic
for file in $env_file; do
  echo "==== $file ===="
  sed -n '1,200p' "$file"
done

Length of output: 912


Ensure BASE_URL is validated and enforce size limits

There is currently no validation on process.env.BASE_URL in envconfig.ts, and the proxy middleware has no request/response size limits. Both gaps can expose the service to misconfiguration or DoS vectors.

Recommended actions:

  • In frontend/apps/server/src/envconfig.ts: validate that BASE_URL is present and a well-formed HTTP(S) URL. For example, use Zod or a simple regex:
    import { z } from 'zod';
    
    const EnvSchema = z.object({
      BASE_URL: z.string().url(),
      AES_GCM_KEY_OF_DISCUSSION_ID: z.string().min(16),
    });
    export const serverEnv = EnvSchema.parse({
      BASE_URL: process.env.BASE_URL,
      AES_GCM_KEY_OF_DISCUSSION_ID: process.env.AES_GCM_KEY_OF_DISCUSSION_ID,
    });
  • In your Express setup (before the proxy), limit payload size:
    import express from 'express';
    const app = express();
    app.use(express.json({ limit: '1mb' }));
    app.use(express.urlencoded({ limit: '1mb', extended: true }));
    // then mount discussionIdTransformer…
  • Optionally, if you need per-route limits, wrap the proxy in a size-checking middleware or configure your upstream API gateway to cap request/response bodies.

These changes will prevent invalid BASE_URL values and mitigate large-payload DoS risks.

🤖 Prompt for AI Agents
In frontend/apps/server/src/envconfig.ts, add validation for BASE_URL to ensure
it is present and a valid HTTP(S) URL using a schema validation library like
Zod. Then, in the Express server setup before mounting the
discussionIdTransformer proxy middleware in
frontend/apps/server/src/middlewares/discussionId/index.ts, add middleware to
limit JSON and URL-encoded payload sizes to 1mb to prevent large-payload DoS
risks. Optionally, consider adding per-route size limits or configuring the API
gateway for further protection.


export const discussionIdVerifier: RequestHandler = (req, res, next) => {
if (req.path.startsWith(DISCUSSION_CREATE_ENDPOINT)) {
try {
decodeDiscussionIdOfUrl(req.path);
return next();
} catch {
res.status(404).json({ code: 'NOT_FOUND', message: 'Invalid discussion id' });
return;
}
}
return next();
};
Loading