mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-30 20:02:36 +08:00
feat: add custom SQL cleanup for scheduled maintenance (#781)
- Add CustomSqlCleanup type to models - Add validateCustomSql and executeCustomSqlCleanup functions - Add SQL validation: DELETE only, single statement, max 1000 chars - Integrate custom SQL cleanup with scheduled job - Add frontend UI with tabs for basic/custom SQL cleanup - Support i18n for English and Chinese 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,78 @@ import { Context } from 'hono';
|
||||
import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
import { CleanupSettings, CustomSqlCleanup } from '../models';
|
||||
|
||||
// Normalize SQL: trim and remove trailing semicolon
|
||||
const normalizeSql = (sql: string): string => {
|
||||
let normalized = sql.trim();
|
||||
if (normalized.endsWith(';')) {
|
||||
normalized = normalized.slice(0, -1).trim();
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// Validate custom SQL cleanup statement
|
||||
export const validateCustomSql = (sql: string): { valid: boolean; error?: string } => {
|
||||
if (!sql || !sql.trim()) {
|
||||
return { valid: false, error: "SQL statement is empty" };
|
||||
}
|
||||
|
||||
const trimmedSql = normalizeSql(sql);
|
||||
|
||||
// Check SQL length (max 1000 characters)
|
||||
if (trimmedSql.length > 1000) {
|
||||
return { valid: false, error: "SQL statement is too long (max 1000 characters)" };
|
||||
}
|
||||
|
||||
const sqlUpper = trimmedSql.toUpperCase();
|
||||
|
||||
// Only allow DELETE statements
|
||||
if (!sqlUpper.startsWith('DELETE ')) {
|
||||
return { valid: false, error: "Only DELETE statements are allowed" };
|
||||
}
|
||||
|
||||
// Only allow single statement (no semicolons after trimming)
|
||||
if (trimmedSql.includes(';')) {
|
||||
return { valid: false, error: "Only single SQL statement is allowed" };
|
||||
}
|
||||
|
||||
// Forbid SQL comments
|
||||
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
|
||||
return { valid: false, error: "SQL comments are not allowed" };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
// Execute custom SQL cleanup
|
||||
export const executeCustomSqlCleanup = async (
|
||||
c: Context<HonoCustomType>,
|
||||
customSql: CustomSqlCleanup
|
||||
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
|
||||
if (!customSql || !customSql.sql) {
|
||||
return { success: false, error: "Invalid custom SQL cleanup config" };
|
||||
}
|
||||
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: validation.error };
|
||||
}
|
||||
|
||||
const sql = normalizeSql(customSql.sql);
|
||||
|
||||
try {
|
||||
console.log(`Executing custom SQL cleanup [${customSql.name}]: ${sql}`);
|
||||
const result = await c.env.DB.prepare(sql).run();
|
||||
const rowsAffected = result.meta?.changes ?? 0;
|
||||
console.log(`Custom SQL cleanup [${customSql.name}] completed, rows affected: ${rowsAffected}`);
|
||||
return { success: true, rowsAffected };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message || "Unknown error";
|
||||
console.error(`Custom SQL cleanup [${customSql.name}] failed:`, errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
cleanup: async (c: Context<HonoCustomType>) => {
|
||||
@@ -22,6 +93,19 @@ export default {
|
||||
},
|
||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||
|
||||
// Validate custom SQL cleanup list
|
||||
if (cleanupSetting.customSqlCleanupList && cleanupSetting.customSqlCleanupList.length > 0) {
|
||||
for (const customSql of cleanupSetting.customSqlCleanupList) {
|
||||
if (customSql.sql) {
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
return c.text(`Invalid SQL [${customSql.name || 'unnamed'}]: ${validation.error}`, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||
return c.json({ success: true })
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ export type WebhookMail = {
|
||||
parsedHtml: string;
|
||||
}
|
||||
|
||||
export type CustomSqlCleanup = {
|
||||
id: string; // Unique identifier
|
||||
name: string; // Cleanup task name
|
||||
sql: string; // Custom SQL statement (DELETE only)
|
||||
enabled: boolean; // Whether to enable auto cleanup
|
||||
}
|
||||
|
||||
export type CleanupSettings = {
|
||||
|
||||
enableMailsAutoCleanup: boolean | undefined;
|
||||
@@ -50,6 +57,7 @@ export type CleanupSettings = {
|
||||
cleanUnboundAddressDays: number;
|
||||
enableEmptyAddressAutoCleanup: boolean | undefined;
|
||||
cleanEmptyAddressDays: number;
|
||||
customSqlCleanupList: CustomSqlCleanup[] | undefined;
|
||||
}
|
||||
|
||||
export class GeoData {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
import { executeCustomSqlCleanup } from './admin_api/cleanup_api';
|
||||
|
||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||
console.log("Scheduled event: ", event);
|
||||
@@ -64,4 +65,18 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
|
||||
autoCleanupSetting.cleanEmptyAddressDays
|
||||
);
|
||||
}
|
||||
// Execute custom SQL cleanup tasks
|
||||
if (autoCleanupSetting.customSqlCleanupList && autoCleanupSetting.customSqlCleanupList.length > 0) {
|
||||
for (const customSql of autoCleanupSetting.customSqlCleanupList) {
|
||||
if (customSql.enabled && customSql.sql) {
|
||||
const result = await executeCustomSqlCleanup(
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
customSql
|
||||
);
|
||||
if (!result.success) {
|
||||
console.error(`Custom SQL cleanup [${customSql.name}] failed: ${result.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user