mirror of
https://github.com/cliffe/BreakEscape.git
synced 2026-02-20 13:50:46 +00:00
feat: Add client-side API integration layer
- Add config.js for API configuration and CSRF token handling - Add api-client.js wrapper for server communication - Add state-sync.js for periodic state synchronization - Support multiple CSRF token sources (config object and meta tag) - Provide detailed error messages for configuration issues - Enable GET/POST/PUT requests with proper auth headers - Expose ApiClient globally for game code integration
This commit is contained in:
114
public/break_escape/js/api-client.js
Normal file
114
public/break_escape/js/api-client.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { API_BASE, CSRF_TOKEN } from './config.js';
|
||||
|
||||
/**
|
||||
* API Client for BreakEscape server communication
|
||||
*/
|
||||
export class ApiClient {
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
static async get(endpoint) {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
static async post(endpoint, data = {}) {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(error.error || `API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
static async put(endpoint, data = {}) {
|
||||
const response = await fetch(`${API_BASE}${endpoint}`, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': CSRF_TOKEN
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Bootstrap - get initial game data
|
||||
static async bootstrap() {
|
||||
return this.get('/bootstrap');
|
||||
}
|
||||
|
||||
// Get scenario JSON
|
||||
static async getScenario() {
|
||||
return this.get('/scenario');
|
||||
}
|
||||
|
||||
// Get NPC script
|
||||
static async getNPCScript(npcId) {
|
||||
return this.get(`/ink?npc=${npcId}`);
|
||||
}
|
||||
|
||||
// Validate unlock attempt
|
||||
static async unlock(targetType, targetId, attempt, method) {
|
||||
return this.post('/unlock', {
|
||||
targetType,
|
||||
targetId,
|
||||
attempt,
|
||||
method
|
||||
});
|
||||
}
|
||||
|
||||
// Update inventory
|
||||
static async updateInventory(action, item) {
|
||||
return this.post('/inventory', {
|
||||
action,
|
||||
item
|
||||
});
|
||||
}
|
||||
|
||||
// Sync player state
|
||||
static async syncState(currentRoom, globalVariables) {
|
||||
return this.put('/sync_state', {
|
||||
currentRoom,
|
||||
globalVariables
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Export for global access
|
||||
window.ApiClient = ApiClient;
|
||||
41
public/break_escape/js/config.js
Normal file
41
public/break_escape/js/config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// API configuration from server
|
||||
export const GAME_ID = window.breakEscapeConfig?.gameId;
|
||||
export const API_BASE = window.breakEscapeConfig?.apiBasePath || '';
|
||||
export const ASSETS_PATH = window.breakEscapeConfig?.assetsPath || '/break_escape/assets';
|
||||
|
||||
// CSRF Token - Try multiple sources (in order of preference)
|
||||
export const CSRF_TOKEN =
|
||||
window.breakEscapeConfig?.csrfToken || // From config object (if set in view)
|
||||
document.querySelector('meta[name="csrf-token"]')?.content; // From meta tag (Hacktivity layout)
|
||||
|
||||
// Verify critical config loaded
|
||||
if (!GAME_ID) {
|
||||
console.error('❌ CRITICAL: Game ID not configured! Check window.breakEscapeConfig');
|
||||
console.error('Expected window.breakEscapeConfig.gameId to be set by server');
|
||||
}
|
||||
|
||||
if (!CSRF_TOKEN) {
|
||||
console.error('❌ CRITICAL: CSRF token not found!');
|
||||
console.error('This will cause all POST/PUT requests to fail with 422 status');
|
||||
console.error('Checked:');
|
||||
console.error(' 1. window.breakEscapeConfig.csrfToken');
|
||||
console.error(' 2. meta[name="csrf-token"] tag');
|
||||
console.error('');
|
||||
console.error('Solutions:');
|
||||
console.error(' - If using Hacktivity layout: Ensure layout has <%= csrf_meta_tags %>');
|
||||
console.error(' - If standalone: Add <%= csrf_meta_tags %> to layout OR');
|
||||
console.error(' - Set window.breakEscapeConfig.csrfToken in view');
|
||||
}
|
||||
|
||||
// Log config for debugging
|
||||
if (window.breakEscapeConfig?.debug || !CSRF_TOKEN) {
|
||||
console.log('✓ BreakEscape config validated:', {
|
||||
gameId: GAME_ID,
|
||||
apiBasePath: API_BASE,
|
||||
assetsPath: ASSETS_PATH,
|
||||
csrfToken: CSRF_TOKEN ? `${CSRF_TOKEN.substring(0, 10)}...` : '❌ MISSING',
|
||||
csrfTokenSource: window.breakEscapeConfig?.csrfToken ? 'config object' :
|
||||
(document.querySelector('meta[name="csrf-token"]') ? 'meta tag' : 'NOT FOUND'),
|
||||
debug: window.breakEscapeConfig?.debug || false
|
||||
});
|
||||
}
|
||||
40
public/break_escape/js/state-sync.js
Normal file
40
public/break_escape/js/state-sync.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiClient } from './api-client.js';
|
||||
|
||||
/**
|
||||
* Periodic state synchronization with server
|
||||
*/
|
||||
export class StateSync {
|
||||
constructor(interval = 30000) { // 30 seconds
|
||||
this.interval = interval;
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.timer = setInterval(() => this.sync(), this.interval);
|
||||
console.log('State sync started (every 30s)');
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
async sync() {
|
||||
try {
|
||||
// Get current game state
|
||||
const currentRoom = window.currentRoom?.name;
|
||||
const globalVariables = window.gameState?.globalVariables || {};
|
||||
|
||||
// Sync to server
|
||||
await ApiClient.syncState(currentRoom, globalVariables);
|
||||
console.log('✓ State synced to server');
|
||||
} catch (error) {
|
||||
console.error('State sync failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.stateSync = new StateSync();
|
||||
Reference in New Issue
Block a user