码桶

发现社区成员的开源项目

dns_records.js6.9 KB
import { logAudit } from '../../_audit.js';
import { saveSnapshot } from '../../_snapshot.js';
import { fireWebhook } from '../../_webhook.js';

const VALID_DNS_TYPES = new Set([
    'A', 'AAAA', 'CNAME', 'TXT', 'MX', 'NS', 'SRV', 'CAA', 'PTR', 'SPF',
    'LOC', 'NAPTR', 'CERT', 'DNSKEY', 'DS', 'HTTPS', 'SSHFP', 'SVCB', 'TLSA', 'URI'
]);

function validateDnsRecord(body) {
    const errors = [];

    if (!body.type || !VALID_DNS_TYPES.has(body.type)) {
        errors.push(`Invalid record type: "${body.type || ''}". Must be one of: ${[...VALID_DNS_TYPES].join(', ')}`);
    }

    if (!body.name || typeof body.name !== 'string' || body.name.trim().length === 0) {
        errors.push('Record name is required.');
    } else if (body.name.length > 253) {
        errors.push(`Record name exceeds maximum length of 253 characters (got ${body.name.length}).`);
    }

    if (!body.content || typeof body.content !== 'string' || body.content.trim().length === 0) {
        errors.push('Record content is required.');
    } else if (body.content.length > 4096) {
        errors.push(`Record content exceeds maximum length of 4096 characters (got ${body.content.length}).`);
    }

    if (body.ttl !== undefined && body.ttl !== null) {
        const ttl = Number(body.ttl);
        if (!Number.isInteger(ttl) || ttl < 1) {
            errors.push('TTL must be a positive integer (use 1 for automatic).');
        }
    }

    if (body.priority !== undefined && body.priority !== null) {
        const priority = Number(body.priority);
        if (!Number.isInteger(priority) || priority < 0 || priority > 65535) {
            errors.push('Priority must be an integer between 0 and 65535.');
        }
    }

    return errors;
}

export async function onRequestGet(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;

    const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?per_page=100`, {
        headers: {
            ...cfHeaders,
            'Content-Type': 'application/json'
        }
    });

    const data = await response.json();

    // Cache DNS record count to KV (fire-and-forget)
    if (data.success && data.result && context.env.CF_DNS_KV) {
        context.env.CF_DNS_KV.put(`DNS_COUNT:${zoneId}`, String(data.result.length), { expirationTtl: 86400 * 30 }).catch(() => {});
    }

    return new Response(JSON.stringify(data), {
        status: response.status,
        headers: { 'Content-Type': 'application/json' }
    });
}

export async function onRequestPost(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    const body = await context.request.json();
    const username = context.data.user?.username || 'client';
    const kv = context.env.CF_DNS_KV;

    // Validate input
    const validationErrors = validateDnsRecord(body);
    if (validationErrors.length > 0) {
        return new Response(JSON.stringify({
            success: false,
            errors: validationErrors.map(msg => ({ message: msg })),
            messages: [],
            result: null
        }), { status: 400, headers: { 'Content-Type': 'application/json' } });
    }

    // Snapshot before mutation
    await saveSnapshot(kv, zoneId, username, 'dns.create', cfHeaders);

    const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, {
        method: 'POST',
        headers: {
            ...cfHeaders,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
    });

    const data = await response.json();
    if (data.success) {
        await logAudit(kv, username, 'dns.create', `${body.type} ${body.name} → ${body.content} (zone: ${zoneId})`);
        await fireWebhook(kv, {
            type: 'dns.create',
            username,
            detail: `${body.type} ${body.name} → ${body.content} (zone: ${zoneId})`
        });
    }
    return new Response(JSON.stringify(data), {
        status: response.status,
        headers: { 'Content-Type': 'application/json' }
    });
}

export async function onRequestPatch(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    const url = new URL(context.request.url);
    const recordId = url.searchParams.get('id');
    const body = await context.request.json();
    const username = context.data.user?.username || 'client';
    const kv = context.env.CF_DNS_KV;

    if (!recordId) return new Response('Missing ID', { status: 400 });

    // Validate input
    const validationErrors = validateDnsRecord(body);
    if (validationErrors.length > 0) {
        return new Response(JSON.stringify({
            success: false,
            errors: validationErrors.map(msg => ({ message: msg })),
            messages: [],
            result: null
        }), { status: 400, headers: { 'Content-Type': 'application/json' } });
    }

    // Snapshot before mutation
    await saveSnapshot(kv, zoneId, username, 'dns.update', cfHeaders);

    const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`, {
        method: 'PATCH',
        headers: {
            ...cfHeaders,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
    });

    const data = await response.json();
    if (data.success) {
        await logAudit(kv, username, 'dns.update', `${body.type || ''} ${body.name || ''} (zone: ${zoneId}, record: ${recordId})`);
        await fireWebhook(kv, {
            type: 'dns.update',
            username,
            detail: `${body.type || ''} ${body.name || ''} (zone: ${zoneId}, record: ${recordId})`
        });
    }
    return new Response(JSON.stringify(data), {
        status: response.status,
        headers: { 'Content-Type': 'application/json' }
    });
}

export async function onRequestDelete(context) {
    const { cfHeaders } = context.data;
    const { zoneId } = context.params;
    const url = new URL(context.request.url);
    const recordId = url.searchParams.get('id');
    const username = context.data.user?.username || 'client';
    const kv = context.env.CF_DNS_KV;

    if (!recordId) return new Response('Missing ID', { status: 400 });

    // Snapshot before mutation
    await saveSnapshot(kv, zoneId, username, 'dns.delete', cfHeaders);

    const response = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${recordId}`, {
        method: 'DELETE',
        headers: {
            ...cfHeaders,
            'Content-Type': 'application/json'
        }
    });

    const data = await response.json();
    if (data.success) {
        await logAudit(kv, username, 'dns.delete', `record: ${recordId} (zone: ${zoneId})`);
        await fireWebhook(kv, {
            type: 'dns.delete',
            username,
            detail: `record: ${recordId} (zone: ${zoneId})`
        });
    }
    return new Response(JSON.stringify(data), {
        status: response.status,
        headers: { 'Content-Type': 'application/json' }
    });
}