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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user