码桶

发现社区成员的开源项目

main
run-scheduled.js5.7 KB
// Run Scheduled DNS Changes
// POST /api/run-scheduled — execute pending scheduled changes that are due
// Can be called by a Cron Trigger or manually by an admin

import { buildCfHeaders } from './_cf-api.js';

export async function onRequestPost(context) {
    const { env } = context;
    const kv = env.CF_DNS_KV;

    if (!kv) {
        return new Response(JSON.stringify({ error: 'KV not configured' }), {
            status: 500,
            headers: { 'Content-Type': 'application/json' }
        });
    }

    const now = Date.now();
    const results = [];
    const CLEANUP_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours

    // List all SCHEDULED_CHANGES:* keys
    const listResult = await kv.list({ prefix: 'SCHEDULED_CHANGES:' });
    const keys = listResult.keys || [];

    for (const keyEntry of keys) {
        const key = keyEntry.name;
        const username = key.replace('SCHEDULED_CHANGES:', '');

        const raw = await kv.get(key);
        if (!raw) continue;

        let changes;
        try {
            changes = JSON.parse(raw);
        } catch {
            continue;
        }

        let modified = false;

        for (const change of changes) {
            if (change.status !== 'pending') continue;

            const scheduledTime = new Date(change.scheduledAt).getTime();
            if (isNaN(scheduledTime) || scheduledTime > now) continue;

            // Time to execute this change
            // Resolve the CF token/key for this user
            let tokenEntry = null;
            const accountIndex = change.accountIndex || 0;

            const tokensJson = await kv.get(`USER_TOKENS:${username}`);
            if (tokensJson) {
                const tokens = JSON.parse(tokensJson);
                tokenEntry = tokens.find(t => t.id === accountIndex);
            }

            // Fallback: env vars (for admin)
            if (!tokenEntry && username === 'admin') {
                const envToken = accountIndex > 0 ? env[`CF_API_TOKEN${accountIndex}`] : env.CF_API_TOKEN;
                if (envToken) tokenEntry = { token: envToken };
            }

            const cfHeaders = tokenEntry ? buildCfHeaders(tokenEntry) : null;

            if (!cfHeaders) {
                change.status = 'failed';
                change.error = 'Could not resolve CF API token for this user/account.';
                change.executedAt = new Date().toISOString();
                modified = true;
                results.push({ id: change.id, username, status: 'failed', error: change.error });
                continue;
            }

            try {
                let apiResult;

                if (change.action === 'create') {
                    apiResult = await executeCfApi(
                        `https://api.cloudflare.com/client/v4/zones/${change.zoneId}/dns_records`,
                        'POST',
                        cfHeaders,
                        change.record
                    );
                } else if (change.action === 'update') {
                    apiResult = await executeCfApi(
                        `https://api.cloudflare.com/client/v4/zones/${change.zoneId}/dns_records/${change.recordId}`,
                        'PATCH',
                        cfHeaders,
                        change.record
                    );
                } else if (change.action === 'delete') {
                    apiResult = await executeCfApi(
                        `https://api.cloudflare.com/client/v4/zones/${change.zoneId}/dns_records/${change.recordId}`,
                        'DELETE',
                        cfHeaders,
                        null
                    );
                }

                if (apiResult.success) {
                    change.status = 'completed';
                    change.executedAt = new Date().toISOString();
                    results.push({ id: change.id, username, status: 'completed' });
                } else {
                    change.status = 'failed';
                    change.error = apiResult.errors?.[0]?.message || 'Cloudflare API error';
                    change.executedAt = new Date().toISOString();
                    results.push({ id: change.id, username, status: 'failed', error: change.error });
                }
            } catch (err) {
                change.status = 'failed';
                change.error = err.message || 'Unknown execution error';
                change.executedAt = new Date().toISOString();
                results.push({ id: change.id, username, status: 'failed', error: change.error });
            }

            modified = true;
        }

        // Clean up completed/failed entries older than 24h
        const before = changes.length;
        const filtered = changes.filter(c => {
            if (c.status === 'pending') return true;
            if (c.executedAt) {
                const executedTime = new Date(c.executedAt).getTime();
                if (now - executedTime > CLEANUP_AGE_MS) return false;
            }
            return true;
        });

        if (filtered.length !== before) modified = true;

        if (modified) {
            if (filtered.length === 0) {
                await kv.delete(key);
            } else {
                await kv.put(key, JSON.stringify(filtered));
            }
        }
    }

    return new Response(JSON.stringify({ success: true, processed: results.length, results }), {
        status: 200,
        headers: { 'Content-Type': 'application/json' }
    });
}

async function executeCfApi(url, method, cfHeaders, body) {
    const options = {
        method,
        headers: {
            ...cfHeaders,
            'Content-Type': 'application/json'
        }
    };
    if (body && method !== 'DELETE') {
        options.body = JSON.stringify(body);
    }
    const res = await fetch(url, options);
    return await res.json();
}