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:
@@ -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',
|
||||
},
|
||||
|
||||
@@ -60,8 +60,9 @@ async function handleMessage(
|
||||
|
||||
// /start 명령어는 미니앱 버튼과 함께 전송
|
||||
if (command === '/start') {
|
||||
const hostingUrl = env.HOSTING_SITE_URL || 'https://hosting.anvil.it.com';
|
||||
await sendMessageWithKeyboard(env.BOT_TOKEN, chatId, responseText, [
|
||||
[{ text: '🌐 서비스 보기', web_app: { url: 'https://hosting.anvil.it.com' } }],
|
||||
[{ text: '🌐 서비스 보기', web_app: { url: hostingUrl } }],
|
||||
[{ text: '💬 문의하기', url: 'https://t.me/AnvilForgeBot' }],
|
||||
]);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user