refactor: comprehensive code review fixes and security hardening
Security: - Add CSP headers for HTML reports (style-src 'unsafe-inline') - Restrict origin validation to specific .kappa-d8e.workers.dev domain - Add base64 size limit (100KB) for report data parameter - Implement rejection sampling for unbiased password generation - Add SQL LIKE pattern escaping for tech specs query - Add security warning for plaintext password storage (TODO: encrypt) Performance: - Add Telegram API timeout (10s) with AbortController - Fix rate limiter sorting by resetTime for proper cleanup - Use centralized TIMEOUTS config for VPS provider APIs Features: - Add admin SSH key support for server recovery access - ADMIN_SSH_PUBLIC_KEY for Linode (public key string) - ADMIN_SSH_KEY_ID_VULTR for Vultr (pre-registered key ID) - Add origin validation middleware - Add idempotency key migration Code Quality: - Return 404 status when no servers found - Consolidate error logging to single JSON.stringify call - Import TECH_CATEGORY_WEIGHTS from config.ts - Add escapeLikePattern utility function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,20 +87,22 @@ export class ProvisioningRepository {
|
||||
|
||||
async createServerOrder(
|
||||
userId: number,
|
||||
telegramUserId: string,
|
||||
specId: number,
|
||||
region: string,
|
||||
pricePaid: number,
|
||||
label: string | null,
|
||||
image: string | null
|
||||
image: string | null,
|
||||
idempotencyKey: string | null
|
||||
): Promise<ServerOrder> {
|
||||
const result = await this.userDb
|
||||
.prepare(
|
||||
`INSERT INTO server_orders
|
||||
(user_id, spec_id, status, region, price_paid, label, image, billing_type)
|
||||
VALUES (?, ?, 'pending', ?, ?, ?, ?, 'monthly')
|
||||
(user_id, telegram_user_id, spec_id, status, region, price_paid, label, image, billing_type, idempotency_key, created_at, expires_at)
|
||||
VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, 'monthly', ?, CURRENT_TIMESTAMP, datetime(CURRENT_TIMESTAMP, '+720 hours'))
|
||||
RETURNING *`
|
||||
)
|
||||
.bind(userId, specId, region, pricePaid, label, image)
|
||||
.bind(userId, telegramUserId, specId, region, pricePaid, label, image, idempotencyKey)
|
||||
.first();
|
||||
|
||||
return result as unknown as ServerOrder;
|
||||
@@ -132,7 +134,16 @@ export class ProvisioningRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update order with root password
|
||||
*
|
||||
* SECURITY WARNING: Password is currently stored in plaintext.
|
||||
* TODO: Implement encryption using WebCrypto API (AES-GCM) before production use.
|
||||
* The encryption key should be stored in env.ENCRYPTION_KEY secret.
|
||||
*/
|
||||
async updateOrderRootPassword(orderId: number, rootPassword: string): Promise<void> {
|
||||
// TODO: Encrypt password before storage
|
||||
// const encryptedPassword = await this.encryptPassword(rootPassword, env.ENCRYPTION_KEY);
|
||||
await this.userDb
|
||||
.prepare(
|
||||
`UPDATE server_orders
|
||||
@@ -176,7 +187,9 @@ export class ProvisioningRepository {
|
||||
async getOrdersByUserId(userId: number, limit: number = 20): Promise<ServerOrder[]> {
|
||||
const result = await this.userDb
|
||||
.prepare(
|
||||
'SELECT * FROM server_orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'
|
||||
`SELECT * FROM server_orders
|
||||
WHERE user_id = ? AND status NOT IN ('terminated', 'cancelled')
|
||||
ORDER BY created_at DESC LIMIT ?`
|
||||
)
|
||||
.bind(userId, limit)
|
||||
.all();
|
||||
@@ -184,6 +197,18 @@ export class ProvisioningRepository {
|
||||
return result.results as unknown as ServerOrder[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find order by idempotency key (for duplicate prevention)
|
||||
*/
|
||||
async findOrderByIdempotencyKey(idempotencyKey: string): Promise<ServerOrder | null> {
|
||||
const result = await this.userDb
|
||||
.prepare('SELECT * FROM server_orders WHERE idempotency_key = ?')
|
||||
.bind(idempotencyKey)
|
||||
.first();
|
||||
|
||||
return result as unknown as ServerOrder | null;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Pricing Lookup (cloud-instances-db)
|
||||
// Uses anvil_pricing, anvil_instances, anvil_regions
|
||||
@@ -233,11 +258,13 @@ export class ProvisioningRepository {
|
||||
*/
|
||||
async createOrderWithPayment(
|
||||
userId: number,
|
||||
telegramUserId: string,
|
||||
specId: number,
|
||||
region: string,
|
||||
priceKrw: number,
|
||||
label: string | null,
|
||||
image: string | null
|
||||
image: string | null,
|
||||
idempotencyKey: string | null
|
||||
): Promise<{ orderId: number | null; error?: string }> {
|
||||
try {
|
||||
// Step 1: Check and deduct balance
|
||||
@@ -249,11 +276,13 @@ export class ProvisioningRepository {
|
||||
// Step 2: Create order
|
||||
const order = await this.createServerOrder(
|
||||
userId,
|
||||
telegramUserId,
|
||||
specId,
|
||||
region,
|
||||
priceKrw,
|
||||
label,
|
||||
image
|
||||
image,
|
||||
idempotencyKey
|
||||
);
|
||||
|
||||
return { orderId: order.id };
|
||||
|
||||
Reference in New Issue
Block a user