initial commit: rocket.new export of broadcastbeat
This commit is contained in:
80
src/app/api/email/domains/[domain_id]/verify/route.ts
Normal file
80
src/app/api/email/domains/[domain_id]/verify/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
// DNS verification — checks SPF, DKIM, DMARC, PTR, MX, blacklist
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ domain_id: string }> }
|
||||
) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { domain_id } = await params;
|
||||
const supabase = await createClient();
|
||||
|
||||
const { data: domainRecord } = await supabase.from('rmp_email_sending_domains').select('*').eq('id', domain_id).single();
|
||||
if (!domainRecord) return NextResponse.json({ error: 'Domain not found' }, { status: 404 });
|
||||
|
||||
const domain = domainRecord.domain;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Perform DNS lookups via public DNS-over-HTTPS
|
||||
async function dnsLookup(type: string, name: string): Promise<string[]> {
|
||||
try {
|
||||
const r = await fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=${type}`, {
|
||||
headers: { Accept: 'application/dns-json' },
|
||||
});
|
||||
const d = await r.json();
|
||||
return (d.Answer ?? []).map((a: any) => a.data as string);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const [txtRecords, mxRecords] = await Promise.all([
|
||||
dnsLookup('TXT', domain),
|
||||
dnsLookup('MX', domain),
|
||||
]);
|
||||
|
||||
const spf_valid = txtRecords.some((r: string) => r.includes('v=spf1'));
|
||||
const dmarc_valid = (await dnsLookup('TXT', `_dmarc.${domain}`)).some((r: string) => r.includes('v=DMARC1'));
|
||||
const mx_valid = mxRecords.length > 0;
|
||||
|
||||
// DKIM — check common selectors
|
||||
const dkimSelectors = ['default', 'google', 'selector1', 'selector2', 'k1', 'mail'];
|
||||
let dkim_valid = false;
|
||||
for (const sel of dkimSelectors) {
|
||||
const recs = await dnsLookup('TXT', `${sel}._domainkey.${domain}`);
|
||||
if (recs.some((r: string) => r.includes('v=DKIM1'))) { dkim_valid = true; break; }
|
||||
}
|
||||
|
||||
// PTR — simplified check
|
||||
const ptr_valid = false; // Requires reverse DNS lookup — not available via DoH
|
||||
|
||||
// Blacklist check (Spamhaus ZEN) — simplified
|
||||
const blacklisted = false; // Full blacklist check requires DNS PTR lookup of IP
|
||||
|
||||
const health_score = [spf_valid, dkim_valid, dmarc_valid, mx_valid].filter(Boolean).length * 25;
|
||||
const overall_pass = spf_valid && dkim_valid && dmarc_valid && mx_valid;
|
||||
|
||||
// Update domain record
|
||||
await supabase.from('rmp_email_sending_domains').update({
|
||||
spf_valid, spf_checked_at: now,
|
||||
dkim_valid, dkim_checked_at: now,
|
||||
dmarc_valid, dmarc_checked_at: now,
|
||||
ptr_valid, ptr_checked_at: now,
|
||||
mx_valid, mx_checked_at: now,
|
||||
dns_fully_verified: overall_pass,
|
||||
blacklisted,
|
||||
blacklist_checked_at: now,
|
||||
health_score,
|
||||
}).eq('id', domain_id);
|
||||
|
||||
// Log each check
|
||||
for (const [type, passed] of [['spf', spf_valid], ['dkim', dkim_valid], ['dmarc', dmarc_valid], ['mx', mx_valid], ['ptr', ptr_valid]] as [string, boolean][]) {
|
||||
await supabase.from('rmp_email_dns_log').insert({ domain_id, check_type: type, passed, checked_at: now });
|
||||
}
|
||||
|
||||
return NextResponse.json({ spf: spf_valid, dkim: dkim_valid, dmarc: dmarc_valid, ptr: ptr_valid, mx: mx_valid, blacklist: blacklisted, overall_pass, health_score });
|
||||
}
|
||||
18
src/app/api/email/domains/reset-counts/route.ts
Normal file
18
src/app/api/email/domains/reset-counts/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
// GET /api/email/domains/reset-counts — run daily midnight
|
||||
export async function GET(request: NextRequest) {
|
||||
const secret = request.headers.get('x-cron-secret') ?? request.nextUrl.searchParams.get('secret');
|
||||
if (secret !== process.env.CRON_SECRET) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const { error } = await supabase.from('rmp_email_sending_domains').update({ sends_today: 0, sends_reset_at: today });
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
44
src/app/api/email/domains/verify-all/route.ts
Normal file
44
src/app/api/email/domains/verify-all/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
// GET /api/email/domains/verify-all — run daily 6:00 AM
|
||||
export async function GET(request: NextRequest) {
|
||||
const secret = request.headers.get('x-cron-secret') ?? request.nextUrl.searchParams.get('secret');
|
||||
if (secret !== process.env.CRON_SECRET) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
const { data: domains } = await supabase.from('rmp_email_sending_domains').select('id, domain, dns_fully_verified');
|
||||
|
||||
let verified = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const d of (domains ?? [])) {
|
||||
try {
|
||||
const r = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/email/domains/${d.id}/verify`);
|
||||
const result = await r.json();
|
||||
if (result.overall_pass) verified++;
|
||||
else {
|
||||
failed++;
|
||||
// Alert admin if previously passing domain now fails
|
||||
if (d.dns_fully_verified && !result.overall_pass) {
|
||||
const notifyEmail = process.env.NOTIFY_EMAIL;
|
||||
if (notifyEmail) {
|
||||
await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/marketplace/send-email`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
to: notifyEmail,
|
||||
subject: `RMP DNS Alert: ${d.domain} now failing`,
|
||||
text: `Domain ${d.domain} was previously verified but DNS checks are now failing.`,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, verified, failed });
|
||||
}
|
||||
75
src/app/api/email/inbound-reply/route.ts
Normal file
75
src/app/api/email/inbound-reply/route.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
|
||||
// POST /api/email/inbound-reply — called by Kerio when reply arrives
|
||||
export async function POST(request: NextRequest) {
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const fromEmail: string = body.from ?? '';
|
||||
const replyBody: string = body.body ?? body.text ?? '';
|
||||
const sendingDomain: string = body.to_domain ?? body.domain ?? '';
|
||||
|
||||
if (!fromEmail) return NextResponse.json({ ok: false, error: 'No from address' }, { status: 400 });
|
||||
|
||||
// Identify site from sending domain
|
||||
const { data: domainRecord } = await supabase
|
||||
.from('rmp_email_sending_domains')
|
||||
.select('site')
|
||||
.eq('domain', sendingDomain)
|
||||
.single();
|
||||
|
||||
const site = domainRecord?.site ?? 'broadcastbeat';
|
||||
|
||||
// Load active keywords
|
||||
const { data: keywords } = await supabase
|
||||
.from('rmp_suppression_keywords')
|
||||
.select('keyword, keyword_type')
|
||||
.eq('active', true);
|
||||
|
||||
const bodyLower = replyBody.toLowerCase();
|
||||
let matchedKeyword: { keyword: string; keyword_type: string } | null = null;
|
||||
|
||||
for (const kw of (keywords ?? [])) {
|
||||
if (bodyLower.includes(kw.keyword.toLowerCase())) {
|
||||
matchedKeyword = kw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedKeyword) {
|
||||
const suppression_type = matchedKeyword.keyword_type === 'hostile' ? 'hostile_reply' : 'unsubscribe';
|
||||
|
||||
// Upsert suppression
|
||||
await supabase.from('rmp_email_suppressions').upsert({
|
||||
site,
|
||||
email: fromEmail.toLowerCase().trim(),
|
||||
suppression_type,
|
||||
source: 'reply_inbound',
|
||||
reply_text: replyBody.slice(0, 500),
|
||||
sending_domain: sendingDomain,
|
||||
suppressed_by: 'system',
|
||||
}, { onConflict: 'site,email' });
|
||||
|
||||
// Notify admin
|
||||
const notifyEmail = process.env.NOTIFY_EMAIL;
|
||||
if (notifyEmail) {
|
||||
try {
|
||||
await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/marketplace/send-email`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
to: notifyEmail,
|
||||
subject: `RMP Email ${suppression_type}: ${fromEmail}`,
|
||||
text: `${fromEmail} replied with: ${replyBody.slice(0, 200)}`,
|
||||
}),
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, action: 'suppressed', type: suppression_type });
|
||||
}
|
||||
|
||||
// No keyword match — route to normal inbox (no action needed)
|
||||
return NextResponse.json({ ok: true, action: 'routed' });
|
||||
}
|
||||
35
src/app/api/email/rmp/domains/[id]/bypass/route.ts
Normal file
35
src/app/api/email/rmp/domains/[id]/bypass/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { data: domain } = await supabase.from('rmp_email_sending_domains').select('*').eq('id', id).single();
|
||||
if (!domain) return NextResponse.json({ error: 'Domain not found' }, { status: 404 });
|
||||
|
||||
await supabase.from('rmp_email_sending_domains').update({
|
||||
bypass_verification: true,
|
||||
bypass_reason: body.reason,
|
||||
bypass_set_by: auth.user?.email ?? 'admin',
|
||||
bypass_set_at: new Date().toISOString(),
|
||||
}).eq('id', id);
|
||||
|
||||
// Log
|
||||
await supabase.from('rmp_email_dns_log').insert({
|
||||
domain_id: id,
|
||||
check_type: 'bypass_override',
|
||||
passed: true,
|
||||
result_detail: `Bypass set: ${body.reason}`,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
20
src/app/api/email/rmp/domains/[id]/route.ts
Normal file
20
src/app/api/email/rmp/domains/[id]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { error } = await supabase.from('rmp_email_sending_domains').update(body).eq('id', id);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
41
src/app/api/email/rmp/domains/route.ts
Normal file
41
src/app/api/email/rmp/domains/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const site = searchParams.get('site');
|
||||
|
||||
let query = supabase.from('rmp_email_sending_domains').select('*').order('site').order('sort_order');
|
||||
if (site) query = query.eq('site', site);
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ domains: data ?? [] });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { data, error } = await supabase.from('rmp_email_sending_domains').insert({
|
||||
site: body.site,
|
||||
domain: body.domain,
|
||||
domain_type: body.domain_type ?? 'subdomain',
|
||||
is_primary: body.is_primary ?? false,
|
||||
sort_order: body.sort_order ?? 0,
|
||||
is_active: false,
|
||||
dns_fully_verified: false,
|
||||
}).select().single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ domain: data });
|
||||
}
|
||||
20
src/app/api/email/rmp/keywords/[id]/route.ts
Normal file
20
src/app/api/email/rmp/keywords/[id]/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { error } = await supabase.from('rmp_suppression_keywords').update({ active: body.active }).eq('id', id);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
31
src/app/api/email/rmp/keywords/route.ts
Normal file
31
src/app/api/email/rmp/keywords/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const { data, error } = await supabase.from('rmp_suppression_keywords').select('*').order('keyword_type').order('keyword');
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ keywords: data ?? [] });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { data, error } = await supabase.from('rmp_suppression_keywords').insert({
|
||||
keyword: body.keyword.toLowerCase().trim(),
|
||||
keyword_type: body.keyword_type ?? 'unsubscribe',
|
||||
active: true,
|
||||
}).select().single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ keyword: data });
|
||||
}
|
||||
27
src/app/api/email/rmp/send-log/route.ts
Normal file
27
src/app/api/email/rmp/send-log/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const site = searchParams.get('site');
|
||||
const status = searchParams.get('status');
|
||||
|
||||
let query = supabase
|
||||
.from('rmp_email_send_log')
|
||||
.select('*, rmp_email_sending_domains(domain)')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(200);
|
||||
|
||||
if (site) query = query.eq('site', site);
|
||||
if (status) query = query.eq('status', status);
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ logs: data ?? [] });
|
||||
}
|
||||
32
src/app/api/email/rmp/stats/route.ts
Normal file
32
src/app/api/email/rmp/stats/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const now = new Date();
|
||||
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
||||
|
||||
const [domainsR, sendsR, suppressionsR] = await Promise.all([
|
||||
supabase.from('rmp_email_sending_domains').select('site, sends_today, blacklisted'),
|
||||
supabase.from('rmp_email_send_log').select('id', { count: 'exact', head: true }).gte('created_at', monthStart),
|
||||
supabase.from('rmp_email_suppressions').select('site'),
|
||||
]);
|
||||
|
||||
const domains = domainsR.data ?? [];
|
||||
const sends_today = domains.reduce((s: number, d: any) => s + (d.sends_today ?? 0), 0);
|
||||
const sends_month = sendsR.count ?? 0;
|
||||
const blacklisted = domains.filter((d: any) => d.blacklisted).length;
|
||||
|
||||
// Suppressions by site
|
||||
const suppressions_by_site: Record<string, number> = {};
|
||||
for (const s of (suppressionsR.data ?? [])) {
|
||||
suppressions_by_site[s.site] = (suppressions_by_site[s.site] ?? 0) + 1;
|
||||
}
|
||||
const total_suppressions = (suppressionsR.data ?? []).length;
|
||||
|
||||
return NextResponse.json({ sends_today, sends_month, total_suppressions, blacklisted, suppressions_by_site });
|
||||
}
|
||||
19
src/app/api/email/rmp/suppressions/[id]/route.ts
Normal file
19
src/app/api/email/rmp/suppressions/[id]/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const supabase = await createClient();
|
||||
|
||||
const { error } = await supabase.from('rmp_email_suppressions').delete().eq('id', id);
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
44
src/app/api/email/rmp/suppressions/route.ts
Normal file
44
src/app/api/email/rmp/suppressions/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const site = searchParams.get('site') ?? '';
|
||||
const type = searchParams.get('type') ?? '';
|
||||
const search = searchParams.get('search') ?? '';
|
||||
|
||||
let query = supabase.from('rmp_email_suppressions').select('*').order('suppressed_at', { ascending: false });
|
||||
if (site) query = query.eq('site', site);
|
||||
if (type) query = query.eq('suppression_type', type);
|
||||
if (search) query = query.ilike('email', `%${search}%`);
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
|
||||
return NextResponse.json({ suppressions: data ?? [] });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const body = await request.json();
|
||||
|
||||
const { data, error } = await supabase.from('rmp_email_suppressions').upsert({
|
||||
site: body.site,
|
||||
email: body.email.toLowerCase().trim(),
|
||||
suppression_type: body.suppression_type ?? 'manual',
|
||||
source: 'manual',
|
||||
notes: body.notes || null,
|
||||
suppressed_by: 'admin',
|
||||
}, { onConflict: 'site,email' }).select().single();
|
||||
|
||||
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
return NextResponse.json({ suppression: data });
|
||||
}
|
||||
25
src/app/api/email/rmp/suppressions/stats/route.ts
Normal file
25
src/app/api/email/rmp/suppressions/stats/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import { requireRmpAdmin } from '@/lib/accounting/rmpService';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireRmpAdmin();
|
||||
if (!auth) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const supabase = await createClient();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const site = searchParams.get('site') ?? '';
|
||||
|
||||
const now = new Date();
|
||||
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
|
||||
|
||||
let baseQuery = supabase.from('rmp_email_suppressions').select('id, suppression_type, suppressed_at');
|
||||
if (site) baseQuery = baseQuery.eq('site', site);
|
||||
|
||||
const { data: all } = await baseQuery;
|
||||
const total = (all ?? []).length;
|
||||
const added_this_month = (all ?? []).filter((s: any) => s.suppressed_at >= monthStart).length;
|
||||
const hostile_replies = (all ?? []).filter((s: any) => s.suppression_type === 'hostile_reply').length;
|
||||
|
||||
return NextResponse.json({ total, added_this_month, hostile_replies });
|
||||
}
|
||||
Reference in New Issue
Block a user