accesscodes/src/lib/minecraft-api.ts
2025-12-20 07:47:49 +00:00

233 lines
6.3 KiB
TypeScript

const API_BASE_URL = process.env.NEXT_PUBLIC_MINECRAFT_API_URL || 'http://localhost:6767';
const API_KEY = process.env.NEXT_PUBLIC_MINECRAFT_API_KEY;
interface ApiResponse<T = any> {
success: boolean;
message?: string;
error?: string;
details?: string;
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<string, PlayerIdentifierOutput>();
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<PlayerIdentifierOutput> {
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<PlayerIdentifierOutput> {
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<PlayerIdentifierOutput> {
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<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const url = `${API_BASE_URL}${endpoint}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(options.headers as Record<string, string> || {}),
};
if (API_KEY) {
headers.Authorization = `Bearer ${API_KEY}`;
}
try {
const response = await fetch(url, {
...options,
headers,
});
const data = await response.json();
if (!response.ok) {
return {
success: false,
error: data.error || 'Request failed',
details: data.details,
};
}
return {
success: true,
data,
message: data.message,
};
} catch (error) {
return {
success: false,
error: 'Network error',
details: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async rewardPlayer(playerName: string): Promise<ApiResponse> {
return this.request('/reward', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async addToWhitelist(playerName: string): Promise<ApiResponse> {
return this.request('/api/whitelist/add', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async removeFromWhitelist(playerName: string): Promise<ApiResponse> {
return this.request('/api/whitelist/remove', {
method: 'POST',
body: JSON.stringify({ player: playerName }),
});
}
async getWhitelist(): Promise<ApiResponse<{ whitelist: string }>> {
return this.request('/api/whitelist/list', {
method: 'GET',
});
}
async executeCommand(command: string): Promise<ApiResponse<{ response: string }>> {
return this.request('/api/command', {
method: 'POST',
body: JSON.stringify({ command }),
});
}
}
export const minecraftApi = new MinecraftApiClient();
export type { ApiResponse, PlayerIdentifierInput, PlayerIdentifierOutput };