feat(platform): add Discord onboarding and Velocity gate

This commit is contained in:
dmg
2026-08-01 13:45:18 -04:00
parent 9d305e5dc9
commit c5de0a1810
80 changed files with 4840 additions and 1572 deletions
@@ -0,0 +1,179 @@
import { randomUUID } from "node:crypto";
import { isRequestTimestampFresh, verifyHashedToken } from "@minecraft-account-manager/auth";
import { velocityAccessRequestSchema } from "@minecraft-account-manager/contracts";
import {
appSettings,
events,
ipObservations,
minecraftAccounts,
pluginCredentials,
pluginRequests,
} from "@minecraft-account-manager/database";
import { and, eq, isNull, lt, sql } from "drizzle-orm";
import { NextResponse } from "next/server";
import { db } from "@/lib/database";
const MAX_CLOCK_SKEW_MS = 45_000;
const DEFAULT_DENIAL_MESSAGE = "Please register your Minecraft account before joining.";
export async function POST(request: Request) {
const authorization = request.headers.get("authorization") ?? "";
const token = authorization.startsWith("Bearer ") ? authorization.slice(7).trim() : "";
if (!token) return NextResponse.json({ allowed: false, message: "Unauthorized" }, { status: 401 });
const parsed = velocityAccessRequestSchema.safeParse(await request.json().catch(() => null));
if (!parsed.success) {
return NextResponse.json({ allowed: false, message: "Invalid access request" }, { status: 400 });
}
const input = parsed.data;
const occurredAt = new Date(input.occurredAt);
if (!isRequestTimestampFresh(occurredAt, new Date(), MAX_CLOCK_SKEW_MS)) {
return NextResponse.json({ allowed: false, message: "Expired access request" }, { status: 401 });
}
const [credential] = await db
.select({ secretHash: pluginCredentials.secretHash })
.from(pluginCredentials)
.where(and(eq(pluginCredentials.serverId, input.serverId), isNull(pluginCredentials.revokedAt)))
.limit(1);
if (!credential || !verifyHashedToken(token, credential.secretHash)) {
return NextResponse.json({ allowed: false, message: "Unauthorized" }, { status: 401 });
}
const [settings] = await db.select().from(appSettings).where(eq(appSettings.id, "default")).limit(1);
const denialMessage = settings?.registrationMessage ?? DEFAULT_DENIAL_MESSAGE;
try {
const decision = await db.transaction(async (tx) => {
await tx.delete(pluginRequests).where(lt(pluginRequests.expiresAt, new Date()));
await tx.insert(pluginRequests).values({
requestId: input.requestId,
serverId: input.serverId,
receivedAt: new Date(),
expiresAt: new Date(Date.now() + 5 * 60_000),
});
let [account] = await tx
.select({
id: minecraftAccounts.id,
userId: minecraftAccounts.userId,
minecraftUuid: minecraftAccounts.minecraftUuid,
username: minecraftAccounts.username,
})
.from(minecraftAccounts)
.where(
and(
eq(minecraftAccounts.minecraftUuid, input.minecraftUuid),
isNull(minecraftAccounts.deletedAt),
),
)
.limit(1);
if (!account) {
[account] = await tx
.select({
id: minecraftAccounts.id,
userId: minecraftAccounts.userId,
minecraftUuid: minecraftAccounts.minecraftUuid,
username: minecraftAccounts.username,
})
.from(minecraftAccounts)
.where(
and(
isNull(minecraftAccounts.minecraftUuid),
sql`lower(${minecraftAccounts.username}) = lower(${input.username})`,
isNull(minecraftAccounts.deletedAt),
),
)
.limit(1);
}
if (!account) {
await tx.insert(events).values({
id: randomUUID(),
source: `/velocity/${input.serverId}`,
type: "games.minecraft.account-manager.game.login.denied",
subject: `minecraft-account/${input.minecraftUuid}`,
time: occurredAt,
data: { username: input.username, reason: "not_registered" },
ipAddress: input.ipAddress,
correlationId: input.requestId,
});
await tx.insert(ipObservations).values({
source: "game",
ipAddress: input.ipAddress,
minecraftUuid: input.minecraftUuid,
username: input.username,
classification: "unknown",
observedAt: occurredAt,
});
return { allowed: false as const, message: denialMessage };
}
if (account.minecraftUuid !== input.minecraftUuid || account.username !== input.username) {
await tx
.update(minecraftAccounts)
.set({
minecraftUuid: input.minecraftUuid,
username: input.username,
lastVerifiedAt: occurredAt,
updatedAt: new Date(),
})
.where(eq(minecraftAccounts.id, account.id));
if (account.username !== input.username) {
await tx.insert(events).values({
id: randomUUID(),
source: `/velocity/${input.serverId}`,
type: "games.minecraft.account-manager.minecraft-account.username-changed",
subject: `minecraft-account/${account.id}`,
time: occurredAt,
actorUserId: account.userId,
data: { previousUsername: account.username, username: input.username },
ipAddress: input.ipAddress,
correlationId: input.requestId,
});
}
}
await tx.insert(ipObservations).values({
userId: account.userId,
minecraftAccountId: account.id,
source: "game",
ipAddress: input.ipAddress,
minecraftUuid: input.minecraftUuid,
username: input.username,
classification: "unknown",
observedAt: occurredAt,
});
await tx.insert(events).values({
id: randomUUID(),
source: `/velocity/${input.serverId}`,
type: "games.minecraft.account-manager.game.login.allowed",
subject: `minecraft-account/${account.id}`,
time: occurredAt,
actorUserId: account.userId,
data: {
username: input.username,
minecraftUuid: input.minecraftUuid,
previousUsername: account.username === input.username ? null : account.username,
uuidBackfilled: account.minecraftUuid === null,
},
ipAddress: input.ipAddress,
correlationId: input.requestId,
});
return { allowed: true as const, message: "Account approved." };
});
return NextResponse.json(decision);
} catch (error) {
console.error("Velocity access decision failed", error);
return NextResponse.json(
{ allowed: false, message: denialMessage },
{ status: 503 },
);
}
}