feat: add server provisioning system with Queue

- Add server-provision.ts for async server creation
- Add SERVER_PROVISION_QUEUE with DLQ for reliability
- Add cron job for auto-cleanup of pending orders (5min)
- Add server delete confirmation with inline keyboard
- Update types for server orders, images, and provisioning
- Add server tables to schema (server_orders, server_instances)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-28 20:26:17 +09:00
parent d3b743c3c1
commit 5ba555864a
8 changed files with 1378 additions and 216 deletions

View File

@@ -63,6 +63,10 @@ export async function handleCallbackQuery(
return;
}
// SECURITY NOTE: Price from callback_data is range-validated here (0-10M)
// but real-time price verification happens in executeDomainRegister()
// which fetches current price from Namecheap API before charging
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '등록 처리 중...' });
await editMessageText(
env.BOT_TOKEN,
@@ -127,9 +131,18 @@ ${result.error}
return;
}
const userId = parts[1];
const callbackUserId = parts[1];
const index = parseInt(parts[2], 10);
// SECURITY: Verify callback userId matches the actual user
if (callbackUserId !== telegramUserId) {
await answerCallbackQuery(env.BOT_TOKEN, queryId, {
text: '⚠️ 권한이 없습니다.',
show_alert: true
});
return;
}
if (isNaN(index) || index < 0 || index > 2) {
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '잘못된 선택입니다.' });
return;
@@ -146,7 +159,7 @@ ${result.error}
// 세션 조회
const { getServerSession, deleteServerSession } = await import('../../server-agent');
if (!env.SESSION_KV) {
if (!env.DB) {
await editMessageText(
env.BOT_TOKEN,
chatId,
@@ -156,7 +169,8 @@ ${result.error}
return;
}
const session = await getServerSession(env.SESSION_KV, userId);
// Use verified telegramUserId instead of callback userId
const session = await getServerSession(env.DB, telegramUserId);
if (!session || !session.lastRecommendation) {
await editMessageText(
@@ -177,33 +191,74 @@ ${result.error}
messageId,
'❌ 선택한 서버를 찾을 수 없습니다.'
);
await deleteServerSession(env.SESSION_KV, userId);
await deleteServerSession(env.DB, telegramUserId);
return;
}
// 주문 처리 (현재는 준비 중)
const { executeServerAction } = await import('../../tools/server-tool');
// 잔액 확인
const deposit = await env.DB.prepare(
'SELECT balance FROM user_deposits WHERE user_id = ?'
).bind(user.id).first<{ balance: number }>();
const result = await executeServerAction(
'order',
{
server_id: selected.plan_name, // 임시
region_code: selected.region.code,
label: `${session.collectedInfo.useCase || 'server'}-1`
},
env,
userId
const price = selected.price?.monthly_krw || 0;
if (!deposit || deposit.balance < price) {
await editMessageText(
env.BOT_TOKEN, chatId, messageId,
`❌ 잔액이 부족합니다.
• 서버 가격: ${price.toLocaleString()}원/월
• 현재 잔액: ${(deposit?.balance || 0).toLocaleString()}
• 부족 금액: ${(price - (deposit?.balance || 0)).toLocaleString()}
잔액을 충전 후 다시 시도해주세요.`
);
return;
}
// Queue 확인
if (!env.SERVER_PROVISION_QUEUE) {
await editMessageText(
env.BOT_TOKEN, chatId, messageId,
'❌ 서버 프로비저닝 시스템이 준비되지 않았습니다.'
);
return;
}
// 주문 생성 (DB INSERT)
const { createServerOrder, sendProvisionMessage } = await import('../../server-provision');
const orderId = await createServerOrder(
env.DB,
user.id,
telegramUserId,
selected.pricing_id,
selected.region.code,
'anvil',
price,
`${selected.plan_name} - ${session.collectedInfo?.useCase || 'server'}`
);
// Queue에 메시지 전송
await sendProvisionMessage(env.SERVER_PROVISION_QUEUE, orderId, user.id, telegramUserId);
// 즉시 응답
await editMessageText(
env.BOT_TOKEN,
chatId,
messageId,
`📋 ${selected.plan_name} 신청\n\n${result}`
`📋 <b>서버 주문 접수 완료!</b> (주문 #${orderId})
• 서버: ${selected.plan_name}
• 리전: ${selected.region.name} (${selected.region.code})
• 가격: ${price.toLocaleString()}원/월
⏳ 서버를 생성하고 있습니다... (1-2분 소요)
완료되면 메시지로 알려드릴게요.`
);
// 세션 삭제
await deleteServerSession(env.SESSION_KV, userId);
await deleteServerSession(env.DB, telegramUserId);
return;
}
@@ -215,7 +270,16 @@ ${result.error}
return;
}
const userId = parts[1];
const callbackUserId = parts[1];
// SECURITY: Verify callback userId matches the actual user
if (callbackUserId !== telegramUserId) {
await answerCallbackQuery(env.BOT_TOKEN, queryId, {
text: '⚠️ 권한이 없습니다.',
show_alert: true
});
return;
}
await answerCallbackQuery(env.BOT_TOKEN, queryId, { text: '취소되었습니다.' });
await editMessageText(
@@ -228,13 +292,15 @@ ${result.error}
// 세션 삭제
const { deleteServerSession } = await import('../../server-agent');
if (env.SESSION_KV) {
await deleteServerSession(env.SESSION_KV, userId);
if (env.DB) {
await deleteServerSession(env.DB, telegramUserId);
}
return;
}
// Note: server_delete callback handler removed - now using text-based confirmation
// 알 수 없는 callback data
await answerCallbackQuery(env.BOT_TOKEN, queryId);
}