Skip to content

Commit 5b41025

Browse files
authored
feat(admin): make suspicious domains configurable (#3562)
feat(admin): make suspicious domains filter configurable
1 parent 697bf52 commit 5b41025

3 files changed

Lines changed: 83 additions & 12 deletions

File tree

apps/web/src/app/admin/components/BlacklistedDomains.tsx

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
'use client';
22

33
import { useState, useEffect } from 'react';
4+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
45
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
56
import { useTRPC } from '@/lib/trpc/utils';
67
import { toast } from 'sonner';
78
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
89
import { Button } from '@/components/ui/button';
910
import { Textarea } from '@/components/ui/textarea';
1011
import { Badge } from '@/components/ui/badge';
12+
import { Checkbox } from '@/components/ui/checkbox';
13+
import { Label } from '@/components/ui/label';
1114
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
1215
import {
1316
Table,
@@ -194,23 +197,26 @@ function StatsTab() {
194197

195198
function SuspiciousTab() {
196199
const trpc = useTRPC();
200+
const [hideLegitimateProviders, setHideLegitimateProviders] = useState(true);
197201

198-
const { data, isLoading } = useQuery(trpc.admin.blacklistDomains.suspicious.queryOptions());
202+
const { data, isLoading } = useQuery(
203+
trpc.admin.blacklistDomains.suspicious.queryOptions({ hideLegitimateProviders })
204+
);
199205

200206
const domains = data?.domains ?? [];
201207
const blacklistedCount = domains.filter(d => d.isBlacklisted).length;
202208

203209
return (
204210
<Card>
205211
<CardHeader>
206-
<div className="flex items-center justify-between">
212+
<div className="flex items-start justify-between gap-4">
207213
<div>
208214
<CardTitle>Suspicious Domains</CardTitle>
209215
<CardDescription>
210216
Top 100 registrable domains by blocked account count, then total account count. Only
211-
shows domains where at least 30% of accounts have been blocked, to filter out
212-
legitimate high-volume providers. Use this to spot domains that are accumulating abuse
213-
but aren&apos;t yet blacklisted.
217+
shows domains where at least 30% of accounts have been blocked when the provider-noise
218+
filter is on. Use this to spot domains that are accumulating abuse but aren&apos;t yet
219+
blacklisted.
214220
</CardDescription>
215221
</div>
216222
{data && (
@@ -226,7 +232,17 @@ function SuspiciousTab() {
226232
)}
227233
</div>
228234
</CardHeader>
229-
<CardContent>
235+
<CardContent className="flex flex-col gap-4">
236+
<div className="flex items-center gap-2">
237+
<Checkbox
238+
id="hide-legitimate-providers"
239+
checked={hideLegitimateProviders}
240+
onCheckedChange={checked => setHideLegitimateProviders(checked === true)}
241+
/>
242+
<Label htmlFor="hide-legitimate-providers" className="text-sm font-normal">
243+
Hide legitimate high-volume providers
244+
</Label>
245+
</div>
230246
{isLoading ? (
231247
<div className="text-muted-foreground py-8 text-center text-sm">Loading…</div>
232248
) : domains.length === 0 ? (
@@ -311,11 +327,36 @@ function formatTimestamp(value: string | null): string {
311327
const tabTriggerClass =
312328
'text-muted-foreground hover:text-foreground data-[state=active]:border-foreground data-[state=active]:text-foreground rounded-none border-b-2 border-transparent px-0 py-3 text-sm font-medium transition-colors data-[state=active]:border-0 data-[state=active]:border-b-2 data-[state=active]:bg-transparent data-[state=active]:shadow-none';
313329

330+
const blacklistedDomainsTabs = ['edit', 'stats', 'suspicious'] as const;
331+
type BlacklistedDomainsTab = (typeof blacklistedDomainsTabs)[number];
332+
333+
function isBlacklistedDomainsTab(value: string): value is BlacklistedDomainsTab {
334+
return blacklistedDomainsTabs.some(tab => tab === value);
335+
}
336+
337+
function getTabFromSearchParam(value: string | null): BlacklistedDomainsTab {
338+
if (value && isBlacklistedDomainsTab(value)) {
339+
return value;
340+
}
341+
342+
return 'edit';
343+
}
344+
314345
export function BlacklistedDomains() {
315-
const [activeTab, setActiveTab] = useState('edit');
346+
const pathname = usePathname();
347+
const router = useRouter();
348+
const searchParams = useSearchParams();
349+
const activeTab = getTabFromSearchParam(searchParams.get('tab'));
350+
351+
function handleTabChange(tab: string) {
352+
const nextTab = getTabFromSearchParam(tab);
353+
const params = new URLSearchParams(searchParams);
354+
params.set('tab', nextTab);
355+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
356+
}
316357

317358
return (
318-
<Tabs value={activeTab} onValueChange={setActiveTab}>
359+
<Tabs value={activeTab} onValueChange={handleTabChange}>
319360
<TabsList className="h-auto w-full justify-start gap-6 rounded-none border-b bg-transparent p-0">
320361
<TabsTrigger value="edit" className={tabTriggerClass}>
321362
Edit

apps/web/src/routers/admin/blacklist-domains-router.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,27 @@ describe('admin.blacklistDomains.suspicious', () => {
264264
expect(names).not.toContain('under-threshold.com');
265265
});
266266

267+
it('includes domains below the blocked-percent threshold when provider filtering is disabled', async () => {
268+
for (let i = 0; i < 9; i++) {
269+
await insertTestUser({
270+
google_user_email: `u${i}@hotmail.com`,
271+
email_domain: 'hotmail.com',
272+
});
273+
}
274+
await insertTestUser({
275+
google_user_email: 'blocked@hotmail.com',
276+
email_domain: 'hotmail.com',
277+
blocked_reason: 'abuse',
278+
});
279+
280+
const caller = await createCallerForUser(admin.id);
281+
const { domains } = await caller.admin.blacklistDomains.suspicious({
282+
hideLegitimateProviders: false,
283+
});
284+
285+
expect(domains.map(d => d.domain)).toContain('hotmail.com');
286+
});
287+
267288
it('excludes users whose email_domain is NULL', async () => {
268289
await insertTestUser({
269290
google_user_email: 'a@example.com',

apps/web/src/routers/admin/blacklist-domains-router.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import { TRPCError } from '@trpc/server';
1212
import { db } from '@/lib/drizzle';
1313
import { kilocode_users } from '@kilocode/db/schema';
1414
import { sql, count, isNotNull, desc, min, max } from 'drizzle-orm';
15+
import * as z from 'zod';
16+
17+
const SuspiciousDomainsInputSchema = z
18+
.object({
19+
hideLegitimateProviders: z.boolean().default(true),
20+
})
21+
.optional();
1522

1623
async function readConfig(): Promise<BlacklistDomainsConfig> {
1724
try {
@@ -67,17 +74,18 @@ export const adminBlacklistDomainsRouter = createTRPCRouter({
6774
return computeBlacklistStats(domains, emailDomainCounts);
6875
}),
6976

70-
suspicious: adminProcedure.query(async () => {
77+
suspicious: adminProcedure.input(SuspiciousDomainsInputSchema).query(async ({ input }) => {
7178
const blacklistedDomains = await getBlacklistedDomains();
7279
const normalizedBlacklist = blacklistedDomains.map(d => d.toLowerCase());
80+
const hideLegitimateProviders = input?.hideLegitimateProviders ?? true;
7381

7482
const blockedCountExpr = sql<number>`count(*) FILTER (WHERE ${kilocode_users.blocked_reason} IS NOT NULL)`;
7583
// Hide noise: require at least 30% of users on the domain to have been
7684
// blocked before surfacing it. Keeps large legitimate providers (gmail,
7785
// hotmail, etc.) from showing up.
7886
const minBlockedPercent = sql`${blockedCountExpr} * 100 >= count(*) * 30`;
7987

80-
const rows = await db
88+
const query = db
8189
.select({
8290
email_domain: kilocode_users.email_domain,
8391
account_count: count(),
@@ -87,8 +95,9 @@ export const adminBlacklistDomainsRouter = createTRPCRouter({
8795
})
8896
.from(kilocode_users)
8997
.where(isNotNull(kilocode_users.email_domain))
90-
.groupBy(kilocode_users.email_domain)
91-
.having(minBlockedPercent)
98+
.groupBy(kilocode_users.email_domain);
99+
100+
const rows = await (hideLegitimateProviders ? query.having(minBlockedPercent) : query)
92101
.orderBy(desc(blockedCountExpr), desc(count()))
93102
.limit(100);
94103

0 commit comments

Comments
 (0)