Skip to content
Merged
76 changes: 76 additions & 0 deletions src/app/admin/api/safety-identifiers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { NextResponse } from 'next/server';
import { getUserFromAuth } from '@/lib/user.server';
import { db } from '@/lib/drizzle';
import { kilocode_users } from '@kilocode/db';
import {
generateOpenRouterUpstreamSafetyIdentifier,
generateVercelDownstreamSafetyIdentifier,
} from '@/lib/providerHash';
import { isNull, count, or, desc, eq } from 'drizzle-orm';

const missingEither = or(
isNull(kilocode_users.openrouter_upstream_safety_identifier),
isNull(kilocode_users.vercel_downstream_safety_identifier)
);

export type SafetyIdentifierCountsResponse = {
missing: number;
};

export type BackfillBatchResponse = {
processed: number;
remaining: boolean;
};

export async function GET(): Promise<
NextResponse<SafetyIdentifierCountsResponse | { error: string }>
> {
const { authFailedResponse } = await getUserFromAuth({ adminOnly: true });
if (authFailedResponse) return authFailedResponse;

const [result] = await db.select({ count: count() }).from(kilocode_users).where(missingEither);

return NextResponse.json({ missing: result?.count ?? 0 });
}

export async function POST(): Promise<NextResponse<BackfillBatchResponse | { error: string }>> {
const { authFailedResponse } = await getUserFromAuth({ adminOnly: true });
if (authFailedResponse) return authFailedResponse;

const processed = await db.transaction(async tran => {
const rows = await tran
Comment thread
chrarnoldus marked this conversation as resolved.
.select({ id: kilocode_users.id })
.from(kilocode_users)
.where(missingEither)
.orderBy(desc(kilocode_users.created_at))
.limit(1000);

for (const user of rows) {
const openrouter_upstream_safety_identifier = generateOpenRouterUpstreamSafetyIdentifier(
Comment thread
chrarnoldus marked this conversation as resolved.
user.id
);
if (openrouter_upstream_safety_identifier === null) {
return null;
}
await tran
.update(kilocode_users)
.set({
openrouter_upstream_safety_identifier,
vercel_downstream_safety_identifier: generateVercelDownstreamSafetyIdentifier(user.id),
})
.where(eq(kilocode_users.id, user.id))
.execute();
}

return rows.length;
});

if (processed === null) {
return NextResponse.json(
{ error: 'OPENROUTER_ORG_ID is not configured on this server' },
{ status: 500 }
);
}

return NextResponse.json({ processed, remaining: processed === 1000 });
}
6 changes: 6 additions & 0 deletions src/app/admin/components/AppSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Bell,
Server,
Network,
KeyRound,
} from 'lucide-react';
import { useSession } from 'next-auth/react';
import type { Session } from 'next-auth';
Expand Down Expand Up @@ -79,6 +80,11 @@ const userManagementItems: MenuItem[] = [
url: '/admin/blacklisted-domains',
icon: () => <Shield />,
},
{
title: () => 'Safety Identifiers',
url: '/admin/safety-identifiers',
icon: () => <KeyRound />,
},
];

const financialItems: MenuItem[] = [
Expand Down
99 changes: 99 additions & 0 deletions src/app/admin/components/SafetyIdentifiersBackfill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import type {
SafetyIdentifierCountsResponse,
BackfillBatchResponse,
} from '../api/safety-identifiers/route';

type BatchLog = {
processed: number;
timestamp: Date;
};

export function SafetyIdentifiersBackfill() {
const [logs, setLogs] = useState<BatchLog[]>([]);
const queryClient = useQueryClient();

const { data: counts, isLoading } = useQuery<SafetyIdentifierCountsResponse>({
queryKey: ['safety-identifier-counts'],
queryFn: async () => {
const res = await fetch('/admin/api/safety-identifiers');
return res.json() as Promise<SafetyIdentifierCountsResponse>;
},
refetchInterval: false,
});

const mutation = useMutation<BackfillBatchResponse, Error>({
mutationFn: async () => {
const res = await fetch('/admin/api/safety-identifiers', { method: 'POST' });
if (!res.ok) {
const body = (await res.json()) as { error?: string };
throw new Error(body.error ?? `HTTP ${res.status}`);
}
return res.json() as Promise<BackfillBatchResponse>;
},
onSuccess: data => {
setLogs(prev => [{ processed: data.processed, timestamp: new Date() }, ...prev]);
void queryClient.invalidateQueries({ queryKey: ['safety-identifier-counts'] });
},
});

const isDone = counts?.missing === 0;

return (
<div className="space-y-6">
<p className="text-muted-foreground text-sm">
Backfill safety identifiers for users missing either field. Each click processes up to 1 000
users. Click repeatedly until the counter reaches zero.
</p>

<div className="bg-background rounded-lg border p-6 space-y-4">
<div className="flex items-center gap-3">
<span className="font-medium">Users missing a safety identifier</span>
{isLoading ? (
<Badge variant="secondary">Loading…</Badge>
) : isDone ? (
<Badge variant="default" className="bg-green-600">
All filled
</Badge>
) : (
<Badge variant="destructive">{(counts?.missing ?? 0).toLocaleString()} missing</Badge>
)}
</div>

{mutation.isError && (
<Alert variant="destructive">
<AlertDescription>{mutation.error.message}</AlertDescription>
</Alert>
)}

<Button
onClick={() => mutation.mutate()}
disabled={isLoading || isDone || mutation.isPending}
variant={isDone ? 'outline' : 'default'}
>
{mutation.isPending ? 'Backfilling…' : isDone ? 'Nothing to do' : 'Backfill next 1 000'}
</Button>
</div>

{logs.length > 0 && (
<div className="bg-background rounded-lg border p-4 space-y-2">
<h4 className="text-sm font-medium">Batch log</h4>
<div className="space-y-1 font-mono text-xs">
{logs.map((log, i) => (
<div key={i} className="text-muted-foreground flex gap-2">
<span className="shrink-0">{log.timestamp.toLocaleTimeString()}</span>
<span>processed {log.processed.toLocaleString()} users</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
15 changes: 15 additions & 0 deletions src/app/admin/components/UserAdmin/UserAdminAccountInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ export function UserAdminAccountInfo(user: UserAdminAccountInfoProps) {
<p className="text-muted-foreground">N/A</p>
)}
</div>
<div>
<h4 className="text-muted-foreground text-sm font-medium">
Vercel Downstream Safety Identifier
</h4>
{user.vercel_downstream_safety_identifier ? (
<div className="flex items-center gap-2">
<p className="font-mono text-sm break-all">
{user.vercel_downstream_safety_identifier}
</p>
<CopyTextButton text={user.vercel_downstream_safety_identifier} />
</div>
) : (
<p className="text-muted-foreground">N/A</p>
)}
</div>
</div>
</div>
</CardContent>
Expand Down
24 changes: 24 additions & 0 deletions src/app/admin/safety-identifiers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SafetyIdentifiersBackfill } from '../components/SafetyIdentifiersBackfill';
import AdminPage from '../components/AdminPage';
import { BreadcrumbItem, BreadcrumbPage } from '@/components/ui/breadcrumb';

const breadcrumbs = (
<>
<BreadcrumbItem>
<BreadcrumbPage>Safety Identifiers</BreadcrumbPage>
</BreadcrumbItem>
</>
);

export default function SafetyIdentifiersPage() {
return (
<AdminPage breadcrumbs={breadcrumbs}>
<div className="flex w-full flex-col gap-y-4">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Safety Identifier Backfill</h2>
</div>
<SafetyIdentifiersBackfill />
</div>
</AdminPage>
);
}
2 changes: 2 additions & 0 deletions src/lib/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ describe('User', () => {
linkedin_url: 'https://linkedin.com/in/testuser',
github_url: 'https://github.com/testuser',
openrouter_upstream_safety_identifier: 'openrouter_upstream_safety_identifier',
vercel_downstream_safety_identifier: 'vercel_downstream_safety_identifier',
customer_source: 'A YouTube video',
is_admin: true,
});
Expand All @@ -108,6 +109,7 @@ describe('User', () => {
expect(softDeleted!.github_url).toBeNull();
expect(softDeleted!.discord_server_membership_verified_at).toBeNull();
expect(softDeleted!.openrouter_upstream_safety_identifier).toBeNull();
expect(softDeleted!.vercel_downstream_safety_identifier).toBeNull();
expect(softDeleted!.customer_source).toBeNull();
expect(softDeleted!.api_token_pepper).toBeNull();
expect(softDeleted!.default_model).toBeNull();
Expand Down
40 changes: 0 additions & 40 deletions src/scripts/openrouter/backfill-safety-identifier.ts

This file was deleted.

Loading