feat(network): add ProxyCheck IP intelligence
This commit is contained in:
@@ -7,6 +7,7 @@ import { redirect } from "next/navigation";
|
||||
import { recordUserEvent } from "@/lib/audit";
|
||||
import { db } from "@/lib/database";
|
||||
import { requireCurrentUser } from "@/lib/auth/user-session";
|
||||
import { checkAccountAdditionNetwork, toAuditIpData } from "@/lib/ip-intelligence";
|
||||
|
||||
const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,16}$/;
|
||||
|
||||
@@ -27,6 +28,21 @@ export async function addMinecraftAccount(formData: FormData) {
|
||||
const confirmed = formData.get("confirmUnverified") === "yes";
|
||||
if (!USERNAME_PATTERN.test(requestedUsername)) redirect("/account?error=invalid-username");
|
||||
|
||||
const network = await checkAccountAdditionNetwork();
|
||||
if (!network.allowed) {
|
||||
await recordUserEvent(
|
||||
user,
|
||||
network.reason === "blocked"
|
||||
? "games.minecraft.account-manager.network.vpn-blocked"
|
||||
: "games.minecraft.account-manager.network.classification-unavailable",
|
||||
{
|
||||
attemptedUsername: requestedUsername,
|
||||
ipIntelligence: network.intelligence ? toAuditIpData(network.intelligence) : null,
|
||||
},
|
||||
);
|
||||
redirect(`/account?error=${network.reason === "blocked" ? "vpn-blocked" : "ip-check-unavailable"}`);
|
||||
}
|
||||
|
||||
const profile = await lookupJavaProfile(requestedUsername);
|
||||
if (!profile && !confirmed) redirect(`/account?unverified=${encodeURIComponent(requestedUsername)}`);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { formatDiscordNickname } from "@minecraft-account-manager/minecraft";
|
||||
import { ipObservations, minecraftAccounts } from "@minecraft-account-manager/database";
|
||||
import { ipIntelligence, ipObservations, minecraftAccounts } from "@minecraft-account-manager/database";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
import { logout } from "@/app/auth/actions";
|
||||
import { db } from "@/lib/database";
|
||||
import { requireCurrentUser } from "@/lib/auth/user-session";
|
||||
import { intelligenceSummary } from "@/lib/event-ip-summary";
|
||||
import {
|
||||
addMinecraftAccount,
|
||||
confirmDashboardNickname,
|
||||
@@ -19,6 +20,8 @@ const errorMessages: Record<string, string> = {
|
||||
"unknown-account": "That account is no longer available.",
|
||||
"nickname-not-configured": "Discord nickname updates are not configured.",
|
||||
"nickname-update-failed": "Discord rejected the nickname update. An admin may need to adjust bot permissions.",
|
||||
"vpn-blocked": "Turn off your VPN, proxy, or Tor connection before adding a Minecraft account.",
|
||||
"ip-check-unavailable": "We could not verify your network, so account addition is temporarily blocked.",
|
||||
};
|
||||
|
||||
export default async function AccountPage({
|
||||
@@ -35,8 +38,16 @@ export default async function AccountPage({
|
||||
.where(and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt)))
|
||||
.orderBy(desc(minecraftAccounts.isPrimary), minecraftAccounts.username),
|
||||
db
|
||||
.select()
|
||||
.select({
|
||||
id: ipObservations.id,
|
||||
ipAddress: ipObservations.ipAddress,
|
||||
source: ipObservations.source,
|
||||
classification: ipObservations.classification,
|
||||
observedAt: ipObservations.observedAt,
|
||||
intelligence: ipIntelligence.rawResponse,
|
||||
})
|
||||
.from(ipObservations)
|
||||
.leftJoin(ipIntelligence, eq(ipIntelligence.ipAddress, ipObservations.ipAddress))
|
||||
.where(eq(ipObservations.userId, user.id))
|
||||
.orderBy(desc(ipObservations.observedAt))
|
||||
.limit(20),
|
||||
@@ -126,7 +137,10 @@ export default async function AccountPage({
|
||||
<h2 className="mt-2 border-b border-line pb-4 font-display text-3xl font-black uppercase">Access addresses</h2>
|
||||
{observations.length ? (
|
||||
<div className="divide-y divide-line font-mono text-xs">
|
||||
{observations.map((observation) => <div className="grid grid-cols-[1fr_auto] gap-4 py-4" key={observation.id}><span>{observation.ipAddress}</span><span className="text-muted">{observation.source} · {observation.observedAt.toISOString()}</span></div>)}
|
||||
{observations.map((observation) => {
|
||||
const summary = intelligenceSummary(observation.intelligence);
|
||||
return <div className="grid grid-cols-[1fr_auto] gap-4 py-4" key={observation.id}><div><p>{observation.ipAddress}</p><p className="mt-1 text-[10px] text-muted">{summary.location ?? "Location unavailable"} · {summary.classification ?? observation.classification}</p></div><span className="text-muted">{observation.source} · {observation.observedAt.toISOString()}</span></div>;
|
||||
})}
|
||||
</div>
|
||||
) : <p className="py-6 text-sm text-muted">No web or game access addresses have been recorded yet.</p>}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user