데이터 무결성: - user_deposits.balance >= 0 CHECK 제약조건 - deposit_transactions.depositor_name 최대 50자 제한 - 음수 잔액 방지, 긴 이름 방지 감사 추적: - audit_logs 테이블 생성 - 모든 중요 작업 추적 (user_id, action, resource, details) - 인덱스 추가 (user_id, action, created_at) 프로덕션 안전: - 백업 → 재생성 → 복원 방식 - 롤백 스크립트 포함 - 데이터 유실 방지 로직 - 음수 잔액 데이터 감지 및 로그 마이그레이션 파일: - migrations/001_schema_enhancements.sql (5.5K) - migrations/001_rollback.sql (4.0K) - migrations/AUDIT_LOG_EXAMPLES.ts (11K) - migrations/TEST_RESULTS.md (8.0K) - migrations/README.md (2.8K) 문서: - SCHEMA_MIGRATION_GUIDE.md (13K) - 완전한 배포 가이드 - MIGRATION_SUMMARY.md (9.1K) - 요약 및 체크리스트 로컬 테스트 결과: - ✅ 마이그레이션 성공 (23 commands, <1초) - ✅ CHECK 제약조건 작동 (음수 잔액 거부) - ✅ 길이 제한 작동 (51자 이름 거부) - ✅ audit_logs 테이블 정상 - ✅ 데이터 보존 확인 (users:3, deposits:1, transactions:1) - ✅ 음수 잔액 데이터 감지 (user_id:3, balance:-500) 프로덕션 배포: - 로컬 테스트 완료, 프로덕션 준비 완료 - 배포 전 백업 필수 - 예상 소요 시간: <5분 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
485 lines
11 KiB
TypeScript
485 lines
11 KiB
TypeScript
/**
|
|
* Audit Log Usage Examples
|
|
*
|
|
* This file demonstrates how to use the audit_logs table
|
|
* in the Telegram Bot Workers codebase.
|
|
*/
|
|
|
|
import { Env } from './types';
|
|
|
|
// =============================================================================
|
|
// Helper Function: Create Audit Log Entry
|
|
// =============================================================================
|
|
|
|
async function createAuditLog(
|
|
env: Env,
|
|
params: {
|
|
userId?: number;
|
|
telegramId?: string;
|
|
action: string;
|
|
resourceType?: string;
|
|
resourceId?: number;
|
|
details?: object;
|
|
ipAddress?: string;
|
|
userAgent?: string;
|
|
}
|
|
): Promise<void> {
|
|
const detailsJson = params.details ? JSON.stringify(params.details) : null;
|
|
|
|
await env.DB.prepare(`
|
|
INSERT INTO audit_logs (
|
|
user_id, telegram_id, action, resource_type, resource_id, details, ip_address, user_agent
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).bind(
|
|
params.userId ?? null,
|
|
params.telegramId ?? null,
|
|
params.action,
|
|
params.resourceType ?? null,
|
|
params.resourceId ?? null,
|
|
detailsJson,
|
|
params.ipAddress ?? null,
|
|
params.userAgent ?? null
|
|
).run();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Example 1: Deposit Confirmation Logging
|
|
// =============================================================================
|
|
|
|
async function logDepositConfirmation(
|
|
env: Env,
|
|
userId: number,
|
|
telegramId: string,
|
|
transactionId: number,
|
|
amount: number,
|
|
depositorName: string,
|
|
bankNotificationId?: number
|
|
): Promise<void> {
|
|
await createAuditLog(env, {
|
|
userId,
|
|
telegramId,
|
|
action: 'deposit_confirmed',
|
|
resourceType: 'deposit_transaction',
|
|
resourceId: transactionId,
|
|
details: {
|
|
amount,
|
|
depositor_name: depositorName,
|
|
matched_bank_notification_id: bankNotificationId,
|
|
balance_change: amount
|
|
}
|
|
});
|
|
|
|
console.log(`[AuditLog] Deposit confirmed: ${amount}원 for user ${telegramId}`);
|
|
}
|
|
|
|
// Usage in deposit-agent.ts or index.ts:
|
|
/*
|
|
// After confirming deposit
|
|
await logDepositConfirmation(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
transactionId,
|
|
5000,
|
|
'홍길동',
|
|
bankNotificationId
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Example 2: Domain Registration Logging
|
|
// =============================================================================
|
|
|
|
async function logDomainRegistration(
|
|
env: Env,
|
|
userId: number,
|
|
telegramId: string,
|
|
domainId: number,
|
|
domain: string,
|
|
price: number,
|
|
balanceBefore: number,
|
|
balanceAfter: number
|
|
): Promise<void> {
|
|
await createAuditLog(env, {
|
|
userId,
|
|
telegramId,
|
|
action: 'domain_registered',
|
|
resourceType: 'user_domains',
|
|
resourceId: domainId,
|
|
details: {
|
|
domain,
|
|
price,
|
|
balance_before: balanceBefore,
|
|
balance_after: balanceAfter,
|
|
balance_change: -price
|
|
}
|
|
});
|
|
|
|
console.log(`[AuditLog] Domain registered: ${domain} for ${price}원`);
|
|
}
|
|
|
|
// Usage in domain-register.ts:
|
|
/*
|
|
// After successful registration
|
|
await logDomainRegistration(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
domainId,
|
|
'example.com',
|
|
15000,
|
|
50000,
|
|
35000
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Example 3: Balance Change Logging
|
|
// =============================================================================
|
|
|
|
async function logBalanceChange(
|
|
env: Env,
|
|
userId: number,
|
|
telegramId: string,
|
|
changeType: 'deposit' | 'withdrawal' | 'refund',
|
|
amount: number,
|
|
balanceBefore: number,
|
|
balanceAfter: number,
|
|
reason: string,
|
|
relatedResourceType?: string,
|
|
relatedResourceId?: number
|
|
): Promise<void> {
|
|
await createAuditLog(env, {
|
|
userId,
|
|
telegramId,
|
|
action: `balance_${changeType}`,
|
|
resourceType: relatedResourceType,
|
|
resourceId: relatedResourceId,
|
|
details: {
|
|
amount,
|
|
balance_before: balanceBefore,
|
|
balance_after: balanceAfter,
|
|
balance_change: changeType === 'withdrawal' ? -amount : amount,
|
|
reason
|
|
}
|
|
});
|
|
|
|
console.log(`[AuditLog] Balance ${changeType}: ${amount}원, new balance: ${balanceAfter}원`);
|
|
}
|
|
|
|
// Usage example:
|
|
/*
|
|
// After domain registration (withdrawal)
|
|
await logBalanceChange(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
'withdrawal',
|
|
15000,
|
|
50000,
|
|
35000,
|
|
'Domain registration: example.com',
|
|
'user_domains',
|
|
domainId
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Example 4: Failed Operations Logging
|
|
// =============================================================================
|
|
|
|
async function logFailedOperation(
|
|
env: Env,
|
|
telegramId: string,
|
|
action: string,
|
|
reason: string,
|
|
details?: object
|
|
): Promise<void> {
|
|
await createAuditLog(env, {
|
|
telegramId,
|
|
action: `${action}_failed`,
|
|
details: {
|
|
reason,
|
|
...details
|
|
}
|
|
});
|
|
|
|
console.error(`[AuditLog] Operation failed: ${action} - ${reason}`);
|
|
}
|
|
|
|
// Usage example:
|
|
/*
|
|
// When deposit confirmation fails
|
|
await logFailedOperation(
|
|
env,
|
|
telegramId,
|
|
'deposit_confirm',
|
|
'Transaction not found',
|
|
{ transaction_id: 123 }
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Example 5: Admin Actions Logging
|
|
// =============================================================================
|
|
|
|
async function logAdminAction(
|
|
env: Env,
|
|
adminTelegramId: string,
|
|
action: string,
|
|
targetUserId?: number,
|
|
targetTelegramId?: string,
|
|
details?: object
|
|
): Promise<void> {
|
|
await createAuditLog(env, {
|
|
telegramId: adminTelegramId,
|
|
action: `admin_${action}`,
|
|
details: {
|
|
target_user_id: targetUserId,
|
|
target_telegram_id: targetTelegramId,
|
|
admin_telegram_id: adminTelegramId,
|
|
...details
|
|
}
|
|
});
|
|
|
|
console.log(`[AuditLog] Admin action: ${action} by ${adminTelegramId}`);
|
|
}
|
|
|
|
// Usage example:
|
|
/*
|
|
// When admin manually confirms deposit
|
|
await logAdminAction(
|
|
env,
|
|
adminTelegramId,
|
|
'manual_deposit_confirm',
|
|
userId,
|
|
userTelegramId,
|
|
{
|
|
transaction_id: transactionId,
|
|
amount: 5000,
|
|
reason: 'Manual verification'
|
|
}
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Query Examples: Retrieve Audit Logs
|
|
// =============================================================================
|
|
|
|
// Get user's recent activity
|
|
async function getUserActivity(
|
|
env: Env,
|
|
telegramId: string,
|
|
limit: number = 10
|
|
) {
|
|
const logs = await env.DB.prepare(`
|
|
SELECT action, resource_type, details, created_at
|
|
FROM audit_logs
|
|
WHERE telegram_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
`).bind(telegramId, limit).all();
|
|
|
|
return logs.results;
|
|
}
|
|
|
|
// Get all deposit confirmations today
|
|
async function getTodayDepositConfirmations(env: Env) {
|
|
const logs = await env.DB.prepare(`
|
|
SELECT
|
|
a.telegram_id,
|
|
a.details,
|
|
a.created_at,
|
|
u.username
|
|
FROM audit_logs a
|
|
LEFT JOIN users u ON a.user_id = u.id
|
|
WHERE a.action = 'deposit_confirmed'
|
|
AND date(a.created_at) = date('now')
|
|
ORDER BY a.created_at DESC
|
|
`).all();
|
|
|
|
return logs.results;
|
|
}
|
|
|
|
// Get suspicious activity (multiple failed attempts)
|
|
async function getSuspiciousActivity(env: Env) {
|
|
const suspicious = await env.DB.prepare(`
|
|
SELECT
|
|
telegram_id,
|
|
action,
|
|
COUNT(*) as attempts,
|
|
MAX(created_at) as last_attempt
|
|
FROM audit_logs
|
|
WHERE action LIKE '%_failed'
|
|
AND created_at > datetime('now', '-1 hour')
|
|
GROUP BY telegram_id, action
|
|
HAVING attempts > 5
|
|
ORDER BY attempts DESC
|
|
`).all();
|
|
|
|
return suspicious.results;
|
|
}
|
|
|
|
// Get user's balance history from audit logs
|
|
async function getBalanceHistory(
|
|
env: Env,
|
|
telegramId: string,
|
|
limit: number = 20
|
|
) {
|
|
const history = await env.DB.prepare(`
|
|
SELECT
|
|
action,
|
|
json_extract(details, '$.amount') as amount,
|
|
json_extract(details, '$.balance_before') as balance_before,
|
|
json_extract(details, '$.balance_after') as balance_after,
|
|
json_extract(details, '$.reason') as reason,
|
|
created_at
|
|
FROM audit_logs
|
|
WHERE telegram_id = ?
|
|
AND action LIKE 'balance_%'
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
`).bind(telegramId, limit).all();
|
|
|
|
return history.results;
|
|
}
|
|
|
|
// Get all domain registrations
|
|
async function getDomainRegistrations(env: Env, days: number = 30) {
|
|
const registrations = await env.DB.prepare(`
|
|
SELECT
|
|
a.telegram_id,
|
|
u.username,
|
|
json_extract(a.details, '$.domain') as domain,
|
|
json_extract(a.details, '$.price') as price,
|
|
a.created_at
|
|
FROM audit_logs a
|
|
LEFT JOIN users u ON a.user_id = u.id
|
|
WHERE a.action = 'domain_registered'
|
|
AND a.created_at > datetime('now', '-' || ? || ' days')
|
|
ORDER BY a.created_at DESC
|
|
`).bind(days).all();
|
|
|
|
return registrations.results;
|
|
}
|
|
|
|
// Get admin actions
|
|
async function getAdminActions(env: Env, days: number = 7) {
|
|
const actions = await env.DB.prepare(`
|
|
SELECT
|
|
action,
|
|
details,
|
|
created_at
|
|
FROM audit_logs
|
|
WHERE action LIKE 'admin_%'
|
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
ORDER BY created_at DESC
|
|
`).bind(days).all();
|
|
|
|
return actions.results;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Export for use in main codebase
|
|
// =============================================================================
|
|
|
|
export {
|
|
createAuditLog,
|
|
logDepositConfirmation,
|
|
logDomainRegistration,
|
|
logBalanceChange,
|
|
logFailedOperation,
|
|
logAdminAction,
|
|
getUserActivity,
|
|
getTodayDepositConfirmations,
|
|
getSuspiciousActivity,
|
|
getBalanceHistory,
|
|
getDomainRegistrations,
|
|
getAdminActions
|
|
};
|
|
|
|
// =============================================================================
|
|
// Integration Example: Add to deposit-agent.ts
|
|
// =============================================================================
|
|
|
|
/*
|
|
// In deposit-agent.ts, after confirming deposit:
|
|
|
|
import { logDepositConfirmation, logBalanceChange } from './audit-log-helpers';
|
|
|
|
// ... existing code ...
|
|
|
|
// After updating balance
|
|
await env.DB.prepare(
|
|
'UPDATE user_deposits SET balance = balance + ? WHERE user_id = ?'
|
|
).bind(amount, userId).run();
|
|
|
|
// Log the deposit confirmation
|
|
await logDepositConfirmation(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
transactionId,
|
|
amount,
|
|
depositorName,
|
|
bankNotificationId
|
|
);
|
|
|
|
// Log the balance change
|
|
await logBalanceChange(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
'deposit',
|
|
amount,
|
|
balanceBefore,
|
|
balanceBefore + amount,
|
|
`Deposit from ${depositorName}`,
|
|
'deposit_transaction',
|
|
transactionId
|
|
);
|
|
*/
|
|
|
|
// =============================================================================
|
|
// Integration Example: Add to domain-register.ts
|
|
// =============================================================================
|
|
|
|
/*
|
|
// In domain-register.ts, after successful registration:
|
|
|
|
import { logDomainRegistration, logBalanceChange } from './audit-log-helpers';
|
|
|
|
// ... existing code ...
|
|
|
|
// After registering domain
|
|
await env.DB.prepare(
|
|
'UPDATE user_deposits SET balance = balance - ? WHERE user_id = ?'
|
|
).bind(price, userId).run();
|
|
|
|
// Log the domain registration
|
|
await logDomainRegistration(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
domainId,
|
|
domain,
|
|
price,
|
|
balanceBefore,
|
|
balanceBefore - price
|
|
);
|
|
|
|
// Log the balance withdrawal
|
|
await logBalanceChange(
|
|
env,
|
|
userId,
|
|
telegramId,
|
|
'withdrawal',
|
|
price,
|
|
balanceBefore,
|
|
balanceBefore - price,
|
|
`Domain registration: ${domain}`,
|
|
'user_domains',
|
|
domainId
|
|
);
|
|
*/
|