"use client";
import Link from "next/link";
import { useState } from "react";
import {
Bell,
CheckCircle2,
Database,
KeyRound,
Loader2,
Mail,
MessageSquare,
Plus,
ScrollText,
Send,
ServerCog,
ShieldAlert,
ShieldBan,
ShieldCheck,
ShieldOff,
Trash2,
XCircle,
type LucideIcon,
} from "lucide-react";
import { authErrorMessage } from "@/hooks/use-auth";
import {
useAcceptUnbanRequest,
useCreateDbViewerSession,
useCreateIpBan,
useCurrentUser,
useDeleteIpBan,
useIpBans,
useRejectUnbanRequest,
useSecurityEvents,
useSystemLogs,
useSystemSecrets,
useSystemStatus,
useUnbanRequests,
useUserApiKeys,
} from "@/hooks/use-auth";
import { useCompanies } from "@/hooks/use-companies";
import {
useCreateNotificationDestination,
useDeleteNotificationDestination,
useNotificationDestinations,
useTestNotificationDestination,
useUpdateNotificationDestination,
} from "@/hooks/use-notification-destinations";
import { FormField } from "@/components/ui/form-field";
import { Select } from "@/components/ui/select";
import { SystemSecretRow } from "@/components/ui/system-secret-row";
import { UserApiKeyRow } from "@/components/ui/user-api-key-row";
import { isLocalConvenience } from "@/lib/auth";
import { formatDateTime } from "@/lib/format";
import {
SEVERITY_LABELS,
SEVERITY_LEVELS,
type LinkedCompany,
type LogCategory,
type NotificationType,
type SeverityLevel,
} from "@/lib/types";
function TestResultBadge({ result }: { result: { success: boolean; error: string | null } }) {
return (
{result.success ? "Test sent" : result.error}
);
}
function LinkedCompanyChips({ companies }: { companies: LinkedCompany[] }) {
if (companies.length === 0) {
return (
Not linked to any company — will be removed automatically.
);
}
return (
{companies.map((company) => (
{company.name}
))}
);
}
function DestinationRow({
destination,
}: {
destination: {
id: string;
type: NotificationType;
destination_value: string;
enabled: boolean;
verified: boolean;
minimum_severity: SeverityLevel;
companies: LinkedCompany[];
};
}) {
const update = useUpdateNotificationDestination();
const remove = useDeleteNotificationDestination();
const test = useTestNotificationDestination();
const Icon = destination.type === "sms" ? MessageSquare : Mail;
return (
{destination.destination_value}
{destination.type} · min severity {destination.minimum_severity}
{test.data && (
)}
);
}
function AddDestinationForm() {
const [type, setType] = useState("email");
const [value, setValue] = useState("");
const [minimumSeverity, setMinimumSeverity] = useState("medium");
const [companyIds, setCompanyIds] = useState>(new Set());
const create = useCreateNotificationDestination();
const { data: systemStatus } = useSystemStatus();
const { data: companies } = useCompanies();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (companyIds.size === 0) return;
create.mutate(
{
type,
destination_value: value,
minimum_severity: minimumSeverity,
company_ids: [...companyIds],
},
{ onSuccess: () => setValue("") },
);
};
const toggleCompany = (id: string) => {
setCompanyIds((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
);
}
const LOG_CATEGORY_STYLES: Record = {
internal_error: { dot: "bg-red-500", label: "Internal error", text: "text-red-700" },
api_error: { dot: "bg-amber-500", label: "API error", text: "text-amber-700" },
important: { dot: "bg-blue-500", label: "Important", text: "text-blue-700" },
normal: { dot: "bg-slate-300", label: "Normal", text: "text-slate-500" },
};
function LoggingBox() {
const { data: logs, isLoading } = useSystemLogs();
return (
Logging
A live feed of what the application is doing, most recent first.
{isLoading ? (
Loading…
) : logs && logs.length > 0 ? (
{logs.map((log, i) => {
const style = LOG_CATEGORY_STYLES[log.category];
return (
-
{formatDateTime(log.ts)}
{log.logger}
{log.event}
);
})}
) : (
No log entries yet.
)}
);
}
function UserApiKeysBox() {
const { data: apiKeys, isLoading } = useUserApiKeys();
const { data: systemStatus } = useSystemStatus();
return (
Your API keys
Set your own key for each provider below to power discovery, enrichment, and monitoring for
your companies.
{isLoading ? (
Loading…
) : (
{apiKeys?.map((k) => (
))}
)}
);
}
function ServerSecretsBox() {
const { data: secrets, isLoading } = useSystemSecrets();
return (
Server secrets
Server-wide configuration shared by every visitor: Cloudflare Turnstile CAPTCHA and the
Resend API key used for transactional security email.
{isLoading ? (
Loading…
) : (
{secrets?.map((s) => )}
)}
);
}
const SECURITY_EVENT_LABELS: Record = {
login_success: "Signed in",
login_failed: "Failed sign-in attempt",
account_locked: "Account locked",
password_reset_requested: "Password reset requested",
password_reset_completed: "Password reset completed",
email_verification_sent: "Verification email sent",
email_verified: "Email verified",
server_secret_updated: "Server secret updated",
api_key_updated: "API key updated",
};
const SECURITY_EVENT_DOT: Record = {
login_success: "bg-green-500",
login_failed: "bg-amber-500",
account_locked: "bg-red-500",
password_reset_requested: "bg-blue-500",
password_reset_completed: "bg-blue-500",
email_verification_sent: "bg-slate-300",
email_verified: "bg-green-500",
server_secret_updated: "bg-purple-500",
api_key_updated: "bg-purple-500",
};
function AccountActivityBox() {
const { data: events, isLoading } = useSecurityEvents();
return (
Account activity
Recent sign-ins and security events for your account.
{isLoading ? (
Loading…
) : events && events.length > 0 ? (
{events.map((event, i) => (
-
{formatDateTime(event.created_at)}
{SECURITY_EVENT_LABELS[event.event_type] ?? event.event_type}
{event.ip_address}
))}
) : (
No activity yet.
)}
);
}
function ActionButton({
onClick,
pending,
disabled,
colorClass,
icon: Icon,
label,
}: {
onClick: () => void;
pending: boolean;
disabled?: boolean;
colorClass: string;
icon: LucideIcon;
label: string;
}) {
return (
);
}
function BanIpForm() {
const [ipAddress, setIpAddress] = useState("");
const createBan = useCreateIpBan();
const handleBan = () => {
if (!ipAddress.trim()) return;
createBan.mutate({ ip_address: ipAddress.trim() }, { onSuccess: () => setIpAddress("") });
};
return (
setIpAddress(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") handleBan();
}}
placeholder="e.g. 203.0.113.42"
className="focus-ring block w-full rounded-md border border-slate-300 px-3 py-1.5 font-mono text-sm text-slate-900 shadow-sm transition-colors duration-150 placeholder:font-sans placeholder:text-slate-400"
/>
{createBan.isError && (
{authErrorMessage(createBan.error)}
)}
);
}
function IpBansBox() {
const { data: bans, isLoading: bansLoading } = useIpBans();
const { data: unbanRequests, isLoading: requestsLoading } = useUnbanRequests();
const deleteBan = useDeleteIpBan();
const acceptRequest = useAcceptUnbanRequest();
const rejectRequest = useRejectUnbanRequest();
return (
IP bans
IPs permanently blocked after repeated abuse, and pending unban requests.
Banned IPs
{bansLoading ? (
Loading…
) : bans && bans.length > 0 ? (
{bans.map((ban) => (
-
{ban.ip_address}
{ban.reason} · banned {formatDateTime(ban.banned_at)}
deleteBan.mutate(ban.ip_address)}
pending={deleteBan.isPending && deleteBan.variables === ban.ip_address}
colorClass="bg-red-600 hover:bg-red-700"
icon={ShieldOff}
label="Unban"
/>
))}
) : (
No IPs currently banned.
)}
Unban requests
{requestsLoading ? (
Loading…
) : unbanRequests && unbanRequests.length > 0 ? (
{unbanRequests.map((req) => (
-
{req.ip_address}
{formatDateTime(req.created_at)}
{req.message &&
{req.message}
}
acceptRequest.mutate(req.id)}
pending={acceptRequest.isPending && acceptRequest.variables === req.id}
disabled={rejectRequest.isPending}
colorClass="bg-green-600 hover:bg-green-700"
icon={CheckCircle2}
label="Unban"
/>
rejectRequest.mutate(req.id)}
pending={rejectRequest.isPending && rejectRequest.variables === req.id}
disabled={acceptRequest.isPending}
colorClass="bg-red-600 hover:bg-red-700"
icon={XCircle}
label="Reject"
/>
))}
) : (
No unban requests.
)}
);
}
function DatabaseViewerBox() {
const { data: systemStatus } = useSystemStatus();
const createSession = useCreateDbViewerSession();
const [error, setError] = useState(null);
const isLocal = systemStatus ? isLocalConvenience(systemStatus) : false;
const dbViewerUrl = process.env.NEXT_PUBLIC_DB_VIEWER_URL;
const handleOpen = async () => {
setError(null);
try {
const { token } = await createSession.mutateAsync();
window.open(`${dbViewerUrl}/_auth?token=${encodeURIComponent(token)}`, "_blank", "noopener,noreferrer");
} catch {
setError("Couldn't open the database viewer. Try again.");
}
};
return (
Database
Open a web-based Postgres client (Adminer) in a new tab to view and edit rows directly.
Local developers and server admins only.
{isLocal ? (
Open database viewer
) : dbViewerUrl ? (
) : (
Not configured for this deployment.
)}
{error &&
{error}
}
);
}
export default function SettingsPage() {
const { data: user } = useCurrentUser();
const { data: systemStatus } = useSystemStatus();
const { data: destinations, isLoading } = useNotificationDestinations();
return (
Settings
Manage where alerts are delivered and check system configuration.
Notification destinations
Alerts are dispatched to every enabled destination whose minimum severity is met.
{isLoading ? (
Loading…
) : destinations && destinations.length > 0 ? (
{destinations.map((d) => (
))}
) : (
No notification destinations yet — add one above.
)}
System configuration
- Account
- {user?.email ?? "—"}
- Auth mode
- {systemStatus?.auth_mode ?? "—"}
- LLM provider
- {systemStatus?.llm_provider ?? "—"}
- Search provider
- {systemStatus?.search_provider ?? "—"}
- SMS delivery
-
{systemStatus?.sms_enabled ? "Enabled" : "Disabled"}
{systemStatus?.sms_enabled && ` (${systemStatus.sms_provider})`}
- Company enrichment (NinjaPear)
-
{systemStatus?.ninjapear_configured
? systemStatus.ninjapear_credit_balance !== null
? `Enabled — ${systemStatus.ninjapear_credit_balance} credits remaining`
: "Enabled — credit balance unavailable"
: "Not configured"}
{systemStatus?.components.map((c) => (
- {c.name}
- {c.status}
))}
{user?.is_admin && (
<>
>
)}
);
}