refactor: code quality improvements (P3)

## Type Safety
- Add zod runtime validation for external API responses
  * Namecheap API responses (domain-register.ts)
  * n8n webhook responses (n8n-service.ts)
  * User request bodies (routes/api.ts)
  * Replaced unsafe type assertions with safeParse()
  * Proper error handling and logging

## Dead Code Removal
- Remove unused callDepositAgent function (127 lines)
  * Legacy Assistants API code no longer needed
  * Now using direct code execution
  * File reduced from 469 → 345 lines (26.4% reduction)

## Configuration Management
- Extract hardcoded URLs to environment variables
  * Added 7 new vars in wrangler.toml:
    OPENAI_API_BASE, NAMECHEAP_API_URL, WHOIS_API_URL,
    CONTEXT7_API_BASE, BRAVE_API_BASE, WTTR_IN_URL, HOSTING_SITE_URL
  * Updated Env interface in types.ts
  * All URLs have fallback to current production values
  * Enables environment-specific configuration (dev/staging/prod)

## Dependencies
- Add zod 4.3.5 for runtime type validation

## Files Modified
- Configuration: wrangler.toml, types.ts, package.json
- Services: 11 TypeScript files with URL/validation updates
- Total: 15 files, +196/-189 lines

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-19 22:06:01 +09:00
parent 4f68dd3ebb
commit 45e0677ab0
15 changed files with 196 additions and 189 deletions

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import { Env } from '../types';
import { sendMessage } from '../telegram';
import {
@@ -11,6 +12,26 @@ import { createLogger } from '../utils/logger';
const logger = createLogger('api');
// Zod schemas for API request validation
const DepositDeductBodySchema = z.object({
telegram_id: z.string(),
amount: z.number().positive(),
reason: z.string(),
reference_id: z.string().optional(),
});
const TestApiBodySchema = z.object({
text: z.string(),
user_id: z.string().optional(),
secret: z.string().optional(),
});
const ContactFormBodySchema = z.object({
email: z.string().email(),
message: z.string(),
name: z.string().optional(),
});
// 사용자 조회/생성
async function getOrCreateUser(
db: D1Database,
@@ -117,20 +138,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json() as {
telegram_id: string;
amount: number;
reason: string;
reference_id?: string;
};
const jsonData = await request.json();
const parseResult = DepositDeductBodySchema.safeParse(jsonData);
if (!body.telegram_id || !body.amount || !body.reason) {
return Response.json({ error: 'telegram_id, amount, reason required' }, { status: 400 });
if (!parseResult.success) {
logger.warn('Deposit deduct - Invalid request body', { errors: parseResult.error.issues });
return Response.json({
error: 'Invalid request body',
details: parseResult.error.issues
}, { status: 400 });
}
if (body.amount <= 0) {
return Response.json({ error: 'Amount must be positive' }, { status: 400 });
}
const body = parseResult.data;
// 사용자 조회
const user = await env.DB.prepare(
@@ -203,7 +222,18 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// 테스트 API - 메시지 처리 후 응답 직접 반환
if (url.pathname === '/api/test' && request.method === 'POST') {
try {
const body = await request.json() as { text: string; user_id?: string; secret?: string };
const jsonData = await request.json();
const parseResult = TestApiBodySchema.safeParse(jsonData);
if (!parseResult.success) {
logger.warn('Test API - Invalid request body', { errors: parseResult.error.issues });
return Response.json({
error: 'Invalid request body',
details: parseResult.error.issues
}, { status: 400 });
}
const body = parseResult.data;
// 간단한 인증
if (body.secret !== env.WEBHOOK_SECRET) {
@@ -261,15 +291,15 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// 문의 폼 API (웹사이트용)
if (url.pathname === '/api/contact' && request.method === 'POST') {
// CORS: hosting.anvil.it.com만 허용
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// Origin 헤더 검증 (curl 우회 방지)
const origin = request.headers.get('Origin');
const allowedOrigin = 'https://hosting.anvil.it.com';
if (!origin || origin !== allowedOrigin) {
logger.warn('Contact API - 허용되지 않은 Origin', { origin });
@@ -280,20 +310,23 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
}
try {
const body = await request.json() as {
email: string;
message: string;
};
const jsonData = await request.json();
const parseResult = ContactFormBodySchema.safeParse(jsonData);
// 필수 필드 검증
if (!body.email || !body.message) {
if (!parseResult.success) {
logger.warn('Contact form - Invalid request body', { errors: parseResult.error.issues });
return Response.json(
{ error: '이메일과 메시지는 필수 항목입니다.' },
{
error: '올바르지 않은 요청 형식입니다.',
details: parseResult.error.issues
},
{ status: 400, headers: corsHeaders }
);
}
// 이메일 형식 검증
const body = parseResult.data;
// 이메일 형식 검증 (Zod로 이미 검증됨, 추가 체크)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(body.email)) {
return Response.json(
@@ -341,9 +374,10 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
// CORS preflight for contact API
if (url.pathname === '/api/contact' && request.method === 'OPTIONS') {
const allowedOrigin = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://hosting.anvil.it.com',
'Access-Control-Allow-Origin': allowedOrigin,
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},