fix: apply optimistic locking to deposit API and add weather types
Security (P1): - Add optimistic locking to /api/deposit/deduct endpoint - Prevent race conditions on concurrent balance deductions - Return 409 Conflict on version mismatch with retry hint Type Safety (P1): - Add WttrResponse, WttrCurrentCondition, WttrWeatherDay types - Remove `as any` from weather-tool.ts - Add safety checks for malformed API responses Both P1 issues from security review resolved. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -160,36 +160,50 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
||||
return Response.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 현재 잔액 확인
|
||||
const deposit = await env.DB.prepare(
|
||||
'SELECT balance FROM user_deposits WHERE user_id = ?'
|
||||
).bind(user.id).first<{ balance: number }>();
|
||||
// 현재 잔액과 version 확인 (Optimistic Locking)
|
||||
const current = await env.DB.prepare(
|
||||
'SELECT balance, version FROM user_deposits WHERE user_id = ?'
|
||||
).bind(user.id).first<{ balance: number; version: number }>();
|
||||
|
||||
const currentBalance = deposit?.balance || 0;
|
||||
if (currentBalance < body.amount) {
|
||||
if (!current) {
|
||||
return Response.json({ error: 'User deposit account not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (current.balance < body.amount) {
|
||||
return Response.json({
|
||||
error: 'Insufficient balance',
|
||||
current_balance: currentBalance,
|
||||
current_balance: current.balance,
|
||||
required: body.amount,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 트랜잭션: 잔액 차감 + 거래 기록
|
||||
const results = await env.DB.batch([
|
||||
env.DB.prepare(
|
||||
'UPDATE user_deposits SET balance = balance - ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?'
|
||||
).bind(body.amount, user.id),
|
||||
env.DB.prepare(
|
||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at)
|
||||
VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)`
|
||||
).bind(user.id, body.amount, body.reason),
|
||||
]);
|
||||
// Optimistic Locking: version 조건으로 잔액 차감
|
||||
const balanceUpdate = await env.DB.prepare(
|
||||
'UPDATE user_deposits SET balance = balance - ?, version = version + 1, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND version = ?'
|
||||
).bind(body.amount, user.id, current.version).run();
|
||||
|
||||
// Batch 결과 검증
|
||||
const allSuccessful = results.every(r => r.success && r.meta?.changes && r.meta.changes > 0);
|
||||
if (!allSuccessful) {
|
||||
logger.error('Batch 부분 실패 (외부 API 잔액 차감)', undefined, {
|
||||
results,
|
||||
if (!balanceUpdate.success || balanceUpdate.meta.changes === 0) {
|
||||
logger.warn('Optimistic locking conflict (외부 API 잔액 차감)', {
|
||||
userId: user.id,
|
||||
telegram_id: body.telegram_id,
|
||||
amount: body.amount,
|
||||
expectedVersion: current.version,
|
||||
context: 'api_deposit_deduct'
|
||||
});
|
||||
return Response.json({
|
||||
error: 'Concurrent modification detected',
|
||||
message: '동시 요청 감지 - 다시 시도해주세요'
|
||||
}, { status: 409 });
|
||||
}
|
||||
|
||||
// 거래 기록 INSERT
|
||||
const transactionInsert = await env.DB.prepare(
|
||||
`INSERT INTO deposit_transactions (user_id, type, amount, status, description, confirmed_at)
|
||||
VALUES (?, 'withdrawal', ?, 'confirmed', ?, CURRENT_TIMESTAMP)`
|
||||
).bind(user.id, body.amount, body.reason).run();
|
||||
|
||||
if (!transactionInsert.success) {
|
||||
logger.error('거래 기록 INSERT 실패 (외부 API)', undefined, {
|
||||
userId: user.id,
|
||||
telegram_id: body.telegram_id,
|
||||
amount: body.amount,
|
||||
@@ -202,7 +216,7 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
const newBalance = currentBalance - body.amount;
|
||||
const newBalance = current.balance - body.amount;
|
||||
|
||||
console.log(`[API] Deposit deducted: user=${body.telegram_id}, amount=${body.amount}, reason=${body.reason}`);
|
||||
|
||||
@@ -210,7 +224,7 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
||||
success: true,
|
||||
telegram_id: body.telegram_id,
|
||||
deducted: body.amount,
|
||||
previous_balance: currentBalance,
|
||||
previous_balance: current.balance,
|
||||
new_balance: newBalance,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user