diff --git a/src/app/api/resolve-player/route.ts b/src/app/api/resolve-player/route.ts new file mode 100644 index 0000000..26f83e3 --- /dev/null +++ b/src/app/api/resolve-player/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { minecraftApi, PlayerIdentifierInput } from '@/lib/minecraft-api'; + +export async function POST(request: NextRequest) { + try { + const body: PlayerIdentifierInput = await request.json(); + + // Validate input + if (!body.type || !body.username) { + return NextResponse.json( + { error: 'Missing required fields: type and username' }, + { status: 400 } + ); + } + + if (body.type !== 'java' && body.type !== 'bedrock') { + return NextResponse.json( + { error: 'Invalid type. Must be "java" or "bedrock"' }, + { status: 400 } + ); + } + + const result = await minecraftApi.resolvePlayerIdentifier(body); + + return NextResponse.json({ + success: true, + data: result + }); + + } catch (error) { + console.error('Player resolution error:', error); + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/whitelist/route.ts b/src/app/api/whitelist/route.ts index 8c9b0b4..d3e1fc1 100644 --- a/src/app/api/whitelist/route.ts +++ b/src/app/api/whitelist/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { servertap } from "@/lib/servertap"; +import { minecraftApi } from "@/lib/minecraft-api"; import { verifyAccessCode } from "@/app/actions"; type WhitelistRequest = { @@ -9,6 +10,13 @@ type WhitelistRequest = { accessCode: string; }; +type ResolvedPlayer = { + platform: "java" | "bedrock"; + username: string; + uuid?: string; + xuid?: string; +}; + export async function POST(request: Request) { try { const data = (await request.json()) as WhitelistRequest; @@ -28,25 +36,45 @@ export async function POST(request: Request) { const results: string[] = []; const errors: string[] = []; + const resolvedPlayers: ResolvedPlayer[] = []; - // Handle Java whitelist + // Resolve Java player if provided if ((preference === "java" || preference === "both") && javaName?.trim()) { - const result = await servertap.addToWhitelist(javaName.trim()); - if (result.success) { - results.push(`Successfully whitelisted Java player: ${javaName}`); - } else { - errors.push(`Failed to whitelist Java player ${javaName}: ${result.error}`); + try { + const resolved = await minecraftApi.resolvePlayerIdentifier({ + type: 'java', + username: javaName.trim() + }); + resolvedPlayers.push(resolved); + } catch (error) { + errors.push(`Failed to resolve Java player ${javaName}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } - // Handle Bedrock whitelist (for now, we'll use the same endpoint but could be different) + // Resolve Bedrock player if provided if ((preference === "bedrock" || preference === "both") && bedrockName?.trim()) { - const bedrockNameWithDot = `.${bedrockName.trim()}`; - const result = await servertap.addToWhitelist(bedrockNameWithDot); - if (result.success) { - results.push(`Successfully whitelisted Bedrock player: ${bedrockNameWithDot}`); - } else { - errors.push(`Failed to whitelist Bedrock player ${bedrockNameWithDot}: ${result.error}`); + try { + const resolved = await minecraftApi.resolvePlayerIdentifier({ + type: 'bedrock', + username: bedrockName.trim() + }); + resolvedPlayers.push(resolved); + } catch (error) { + errors.push(`Failed to resolve Bedrock player ${bedrockName}: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + // Add resolved players to whitelist + for (const player of resolvedPlayers) { + try { + const result = await servertap.addToWhitelist(player); + if (result.success) { + results.push(`Successfully whitelisted ${player.platform} player: ${player.username} (${player.uuid || player.xuid})`); + } else { + errors.push(`Failed to whitelist ${player.platform} player ${player.username}: ${result.error}`); + } + } catch (error) { + errors.push(`Failed to whitelist ${player.platform} player ${player.username}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } diff --git a/src/lib/minecraft-api.ts b/src/lib/minecraft-api.ts index 3da823d..e7bc212 100644 --- a/src/lib/minecraft-api.ts +++ b/src/lib/minecraft-api.ts @@ -9,7 +9,147 @@ interface ApiResponse { data?: T; } +interface PlayerIdentifierInput { + type: 'java' | 'bedrock'; + username: string; +} + +interface PlayerIdentifierOutput { + platform: 'java' | 'bedrock'; + username: string; + uuid?: string; + xuid?: string; +} + +interface MojangResponse { + id: string; + name: string; +} + +interface GeyserResponse { + xuid: string; + gamertag: string; +} + class MinecraftApiClient { + private cache = new Map(); + private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + private isCacheValid(timestamp: number): boolean { + return Date.now() - timestamp < this.CACHE_TTL; + } + + private getCacheKey(type: string, username: string): string { + return `${type}:${username.toLowerCase()}`; + } + + private formatUUID(uuid: string): string { + // Convert UUID from Mojang format (no dashes) to dashed format + return `${uuid.slice(0,8)}-${uuid.slice(8,12)}-${uuid.slice(12,16)}-${uuid.slice(16,20)}-${uuid.slice(20)}`; + } + + private async resolveJavaUsername(username: string): Promise { + const cacheKey = this.getCacheKey('java', username); + const cached = this.cache.get(cacheKey); + if (cached && this.isCacheValid((cached as any).timestamp)) { + return cached; + } + + try { + const response = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`, { + headers: { + 'User-Agent': 'MinecraftResolver/1.0' + } + }); + + if (response.status === 404) { + throw new Error('Player not found'); + } + + if (response.status === 429) { + throw new Error('Rate limit exceeded'); + } + + if (!response.ok) { + throw new Error(`Mojang API error: ${response.status}`); + } + + const data: MojangResponse = await response.json(); + const result: PlayerIdentifierOutput = { + platform: 'java', + username: data.name, + uuid: this.formatUUID(data.id) + }; + + // Cache with timestamp + (result as any).timestamp = Date.now(); + this.cache.set(cacheKey, result); + + return result; + } catch (error) { + throw new Error(`Failed to resolve Java username: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + private async resolveBedrockGamertag(gamertag: string): Promise { + const cacheKey = this.getCacheKey('bedrock', gamertag); + const cached = this.cache.get(cacheKey); + if (cached && this.isCacheValid((cached as any).timestamp)) { + return cached; + } + + try { + // Try GeyserMC API first (most reliable for public use) + const response = await fetch(`https://api.geysermc.org/v2/xbox/xuid/${encodeURIComponent(gamertag)}`, { + headers: { + 'User-Agent': 'MinecraftResolver/1.0' + } + }); + + if (response.status === 404) { + throw new Error('Gamertag not found'); + } + + if (response.status === 429) { + throw new Error('Rate limit exceeded'); + } + + if (!response.ok) { + throw new Error(`GeyserMC API error: ${response.status}`); + } + + const data: GeyserResponse = await response.json(); + const result: PlayerIdentifierOutput = { + platform: 'bedrock', + username: data.gamertag, + xuid: data.xuid + }; + + // Cache with timestamp + (result as any).timestamp = Date.now(); + this.cache.set(cacheKey, result); + + return result; + } catch (error) { + throw new Error(`Failed to resolve Bedrock gamertag: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + async resolvePlayerIdentifier(input: PlayerIdentifierInput): Promise { + if (!input.username || input.username.trim().length === 0) { + throw new Error('Username is required'); + } + + const trimmedUsername = input.username.trim(); + + if (input.type === 'java') { + return this.resolveJavaUsername(trimmedUsername); + } else if (input.type === 'bedrock') { + return this.resolveBedrockGamertag(trimmedUsername); + } else { + throw new Error('Invalid platform type. Must be "java" or "bedrock"'); + } + } private async request( endpoint: string, options: RequestInit = {} @@ -90,4 +230,4 @@ class MinecraftApiClient { } export const minecraftApi = new MinecraftApiClient(); -export type { ApiResponse }; +export type { ApiResponse, PlayerIdentifierInput, PlayerIdentifierOutput }; diff --git a/src/lib/servertap.ts b/src/lib/servertap.ts index e236758..8a22600 100644 --- a/src/lib/servertap.ts +++ b/src/lib/servertap.ts @@ -4,6 +4,13 @@ type MinecraftPlayer = { name: string; }; +type ResolvedPlayer = { + platform: "java" | "bedrock"; + username: string; + uuid?: string; + xuid?: string; +}; + type MinecraftResponse = { success: boolean; data?: T; @@ -72,17 +79,61 @@ class MinecraftClient { } } - async addToWhitelist(playerName: string): Promise> { + async addToWhitelist(player: string | ResolvedPlayer): Promise> { + // Handle legacy string input for backward compatibility + if (typeof player === 'string') { + return this.makeRequest("/api/whitelist/add", { + method: "POST", + body: JSON.stringify({ player }), + }); + } + + // Send resolved player data with UUID/XUID + const payload: any = { + player: player.username, + platform: player.platform, + }; + + if (player.uuid) { + payload.uuid = player.uuid; + } + + if (player.xuid) { + payload.xuid = player.xuid; + } + return this.makeRequest("/api/whitelist/add", { method: "POST", - body: JSON.stringify({ player: playerName }), + body: JSON.stringify(payload), }); } - async removeFromWhitelist(playerName: string): Promise> { + async removeFromWhitelist(player: string | ResolvedPlayer): Promise> { + // Handle legacy string input for backward compatibility + if (typeof player === 'string') { + return this.makeRequest("/api/whitelist/remove", { + method: "POST", + body: JSON.stringify({ player }), + }); + } + + // Send resolved player data with UUID/XUID + const payload: any = { + player: player.username, + platform: player.platform, + }; + + if (player.uuid) { + payload.uuid = player.uuid; + } + + if (player.xuid) { + payload.xuid = player.xuid; + } + return this.makeRequest("/api/whitelist/remove", { method: "POST", - body: JSON.stringify({ player: playerName }), + body: JSON.stringify(payload), }); } @@ -111,4 +162,4 @@ export const minecraft = new MinecraftClient(); // Legacy exports for backward compatibility export const servertap = minecraft; -export type { MinecraftPlayer as ServerTapPlayer, MinecraftResponse as ServerTapResponse }; +export type { MinecraftPlayer as ServerTapPlayer, MinecraftResponse as ServerTapResponse, ResolvedPlayer }; diff --git a/test-api.sh b/test-api.sh new file mode 100755 index 0000000..b2bd3da --- /dev/null +++ b/test-api.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Test script for the player resolver API endpoint +# Start the dev server first with: npm run dev + +echo "Testing Java player resolution..." +curl -X POST http://localhost:3000/api/resolve-player \ + -H "Content-Type: application/json" \ + -d '{"type": "java", "username": "Notch"}' \ + | jq . + +echo -e "\nTesting Bedrock player resolution..." +curl -X POST http://localhost:3000/api/resolve-player \ + -H "Content-Type: application/json" \ + -d '{"type": "bedrock", "username": "TestGamertag"}' \ + | jq . + +echo -e "\nTesting invalid input..." +curl -X POST http://localhost:3000/api/resolve-player \ + -H "Content-Type: application/json" \ + -d '{"type": "invalid", "username": ""}' \ + | jq . + +echo -e "\nTesting missing fields..." +curl -X POST http://localhost:3000/api/resolve-player \ + -H "Content-Type: application/json" \ + -d '{}' \ + | jq . diff --git a/test-integrated-whitelist.sh b/test-integrated-whitelist.sh new file mode 100644 index 0000000..8e78a62 --- /dev/null +++ b/test-integrated-whitelist.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Test script for the integrated whitelist with player resolver +# This tests the full flow: resolve player -> send UUID/XUID to server + +echo "Testing integrated whitelist with player resolver..." + +# Test Java player with valid access code (you'll need to replace with actual access code) +echo -e "\n=== Testing Java Player Resolution + Whitelist ===" +curl -X POST http://localhost:3000/api/whitelist \ + -H "Content-Type: application/json" \ + -d '{ + "javaName": "Notch", + "preference": "java", + "accessCode": "YOUR_ACCESS_CODE_HERE" + }' \ + | jq . + +# Test Bedrock player with valid access code +echo -e "\n=== Testing Bedrock Player Resolution + Whitelist ===" +curl -X POST http://localhost:3000/api/whitelist \ + -H "Content-Type: application/json" \ + -d '{ + "bedrockName": "TestGamertag", + "preference": "bedrock", + "accessCode": "YOUR_ACCESS_CODE_HERE" + }' \ + | jq . + +# Test both platforms +echo -e "\n=== Testing Both Platforms Resolution + Whitelist ===" +curl -X POST http://localhost:3000/api/whitelist \ + -H "Content-Type: application/json" \ + -d '{ + "javaName": "Notch", + "bedrockName": "TestGamertag", + "preference": "both", + "accessCode": "YOUR_ACCESS_CODE_HERE" + }' \ + | jq . + +# Test invalid access code +echo -e "\n=== Testing Invalid Access Code ===" +curl -X POST http://localhost:3000/api/whitelist \ + -H "Content-Type: application/json" \ + -d '{ + "javaName": "Notch", + "preference": "java", + "accessCode": "invalid_code" + }' \ + | jq . + +# Test invalid username +echo -e "\n=== Testing Invalid Username ===" +curl -X POST http://localhost:3000/api/whitelist \ + -H "Content-Type: application/json" \ + -d '{ + "javaName": "nonexistentplayer12345", + "preference": "java", + "accessCode": "YOUR_ACCESS_CODE_HERE" + }' \ + | jq . + +echo -e "\n=== Test completed ===" +echo "Note: Replace YOUR_ACCESS_CODE_HERE with a valid access code from your system" diff --git a/test-player-resolver.js b/test-player-resolver.js new file mode 100644 index 0000000..df31849 --- /dev/null +++ b/test-player-resolver.js @@ -0,0 +1,59 @@ +// Simple test script for the player resolver +// Run with: node test-player-resolver.js + +const { minecraftApi } = require('./src/lib/minecraft-api.ts'); + +async function testJavaPlayer() { + console.log('Testing Java player resolution...'); + try { + const result = await minecraftApi.resolvePlayerIdentifier({ + type: 'java', + username: 'Notch' // Famous Minecraft player + }); + console.log('Java player result:', result); + } catch (error) { + console.error('Java player error:', error.message); + } +} + +async function testBedrockPlayer() { + console.log('Testing Bedrock player resolution...'); + try { + const result = await minecraftApi.resolvePlayerIdentifier({ + type: 'bedrock', + username: 'TestGamertag' // This will likely fail but tests the API + }); + console.log('Bedrock player result:', result); + } catch (error) { + console.error('Bedrock player error:', error.message); + } +} + +async function testInvalidInput() { + console.log('Testing invalid input...'); + try { + await minecraftApi.resolvePlayerIdentifier({ + type: 'invalid', + username: '' + }); + } catch (error) { + console.error('Expected error for invalid input:', error.message); + } +} + +async function runTests() { + console.log('Starting player resolver tests...\n'); + + await testJavaPlayer(); + console.log('\n'); + + await testBedrockPlayer(); + console.log('\n'); + + await testInvalidInput(); + console.log('\n'); + + console.log('Tests completed!'); +} + +runTests().catch(console.error);