diff --git a/src/lib/api/adapters/base.adapter.ts b/src/lib/api/adapters/base.adapter.ts index 3c0fbad5..2b9bacd6 100644 --- a/src/lib/api/adapters/base.adapter.ts +++ b/src/lib/api/adapters/base.adapter.ts @@ -18,6 +18,8 @@ import { calculateRetryDelay, CancelledError } from './errors' +import { authStore } from '$lib/stores/auth.store' +import { browser } from '$app/environment' /** * Base adapter class that all resource-specific adapters extend from. @@ -46,6 +48,9 @@ export abstract class BaseAdapter { /** Configuration options for the adapter */ protected options: Required + /** Flag to disable caching entirely */ + protected disableCache: boolean = false + /** * Creates a new adapter instance * @@ -97,8 +102,8 @@ export abstract class BaseAdapter { // Check cache first if caching is enabled (support both cache and cacheTTL) const cacheTime = options.cacheTTL ?? options.cache ?? this.options.cacheTime - // Allow caching for any method if explicitly set - if (cacheTime > 0) { + // Allow caching for any method if explicitly set (unless cache is disabled) + if (!this.disableCache && cacheTime > 0) { const cached = this.getFromCache(requestId) if (cached !== null) { return cached as T @@ -112,26 +117,53 @@ export abstract class BaseAdapter { const controller = new AbortController() this.abortControllers.set(requestId, controller) + // Get Bearer token from auth store (only in browser) + let authHeaders: Record = {} + if (browser) { + const token = await authStore.checkAndRefresh() + if (token) { + authHeaders['Authorization'] = `Bearer ${token}` + } else { + console.warn('[BaseAdapter] No auth token available in authStore for request:', path) + } + } + // Prepare request options const fetchOptions: RequestInit = { - credentials: 'include', // Default: Include cookies for authentication ...options, // Allow overriding defaults + credentials: 'include', // Still include cookies for CORS and refresh token signal: controller.signal, headers: { 'Content-Type': 'application/json', + ...authHeaders, ...(options.headers || {}) } } + // Debug logging for auth issues + if (browser && path.includes('grid_')) { + console.log('[BaseAdapter] Request to:', path, 'Headers:', fetchOptions.headers) + } + // Transform request body from camelCase to snake_case if present if (options.body) { if (typeof options.body === 'object') { // Body is an object, transform and stringify - fetchOptions.body = JSON.stringify(this.transformRequest(options.body)) + const transformed = this.transformRequest(options.body) + fetchOptions.body = JSON.stringify(transformed) + // Debug logging for 422 errors + if (browser && path.includes('grid_')) { + console.log('[BaseAdapter] Request body:', transformed) + } } else if (typeof options.body === 'string') { try { const bodyData = JSON.parse(options.body) - fetchOptions.body = JSON.stringify(this.transformRequest(bodyData)) + const transformed = this.transformRequest(bodyData) + fetchOptions.body = JSON.stringify(transformed) + // Debug logging for 422 errors + if (browser && path.includes('grid_')) { + console.log('[BaseAdapter] Request body:', transformed) + } } catch { // If body is not valid JSON, use as-is fetchOptions.body = options.body @@ -145,10 +177,17 @@ export abstract class BaseAdapter { // Parse and transform the response const data = await response.json() + + // Debug logging for grid operations + if (browser && path.includes('grid_')) { + console.log('[BaseAdapter] Response status:', response.status) + console.log('[BaseAdapter] Response data:', data) + } + const transformed = this.transformResponse(data) // Cache the successful response if caching is enabled (use cacheTTL or cache) - if (cacheTime > 0) { + if (!this.disableCache && cacheTime > 0) { this.setCache(requestId, transformed, cacheTime) } @@ -340,10 +379,32 @@ export abstract class BaseAdapter { // Build URL from base URL and path const baseURL = this.options.baseURL.replace(/\/$/, '') // Remove trailing slash const cleanPath = path.startsWith('/') ? path : `/${path}` - const url = new URL(`${baseURL}${cleanPath}`) + const fullPath = `${baseURL}${cleanPath}` - this.addQueryParams(url, params) - return url.toString() + // Check if we have a relative URL (starts with /) + if (baseURL.startsWith('/')) { + // For relative URLs, we need to provide a base for the URL constructor + // but we'll return just the relative path for fetch + if (typeof window !== 'undefined') { + // In browser, use window.location.origin for URL construction + const url = new URL(fullPath, window.location.origin) + this.addQueryParams(url, params) + // Return just the pathname and search for relative fetch + return `${url.pathname}${url.search}` + } else { + // On server, construct the query string manually for relative paths + if (params && Object.keys(params).length > 0) { + const queryString = new URLSearchParams(this.transformRequest(params)).toString() + return `${fullPath}?${queryString}` + } + return fullPath + } + } else { + // For absolute base URLs, use the normal URL constructor + const url = new URL(fullPath) + this.addQueryParams(url, params) + return url.toString() + } } /** diff --git a/src/lib/api/adapters/config.ts b/src/lib/api/adapters/config.ts index 72e13ae2..fc1d873e 100644 --- a/src/lib/api/adapters/config.ts +++ b/src/lib/api/adapters/config.ts @@ -5,10 +5,10 @@ import { PUBLIC_SIERO_API_URL } from '$env/static/public' /** * Get the base URL for API requests - * Handles both server and client environments + * Always use direct API URL for both server and client */ export function getApiBaseUrl(): string { - // Use environment variable if available, otherwise default to localhost + // Always use direct API URL const base = PUBLIC_SIERO_API_URL || 'http://localhost:3000' return `${base}/api/v1` } diff --git a/src/lib/api/adapters/grid.adapter.ts b/src/lib/api/adapters/grid.adapter.ts index a2cbe922..72d33bfb 100644 --- a/src/lib/api/adapters/grid.adapter.ts +++ b/src/lib/api/adapters/grid.adapter.ts @@ -115,10 +115,10 @@ export interface UpdateUncapParams { * Parameters for updating positions */ export interface UpdatePositionParams { - partyId: string - id: string - position: number - container?: string + partyId: string + id: string + position: number + container?: string } /** @@ -150,196 +150,259 @@ export class GridAdapter extends BaseAdapter { /** * Creates a new grid weapon instance */ - async createWeapon(params: CreateGridWeaponParams): Promise { - return this.request('/grid_weapons', { - method: 'POST', - body: params - }) - } + async createWeapon(params: CreateGridWeaponParams, headers?: Record): Promise { + return this.request('/grid_weapons', { + method: 'POST', + body: { weapon: params }, + headers + }) + } /** * Updates a grid weapon instance */ - async updateWeapon(id: string, params: Partial): Promise { - return this.request(`/grid_weapons/${id}`, { - method: 'PUT', - body: params - }) - } + async updateWeapon(id: string, params: Partial, headers?: Record): Promise { + return this.request(`/grid_weapons/${id}`, { + method: 'PUT', + body: { weapon: params }, + headers + }) + } /** * Deletes a grid weapon instance */ - async deleteWeapon(params: { id?: string; partyId: string; position?: number }): Promise { - return this.request('/grid_weapons', { - method: 'DELETE', - body: params - }) - } + async deleteWeapon(params: { id?: string; partyId: string; position?: number }, headers?: Record): Promise { + // If we have an ID, use it in the URL (standard Rails REST) + if (params.id) { + return this.request(`/grid_weapons/${params.id}`, { + method: 'DELETE', + headers + }) + } + // Otherwise, send params in body for position-based delete + return this.request('/grid_weapons/delete_by_position', { + method: 'DELETE', + body: params, + headers + }) + } /** * Updates weapon uncap level */ - async updateWeaponUncap(params: UpdateUncapParams): Promise { - return this.request('/grid_weapons/update_uncap', { - method: 'POST', - body: params - }) - } + async updateWeaponUncap(params: UpdateUncapParams, headers?: Record): Promise { + return this.request('/grid_weapons/update_uncap', { + method: 'POST', + body: { + weapon: { + id: params.id, + partyId: params.partyId, + uncapLevel: params.uncapLevel, + transcendenceStep: params.transcendenceStep + } + }, + headers + }) + } /** * Resolves weapon conflicts */ - async resolveWeaponConflict(params: ResolveConflictParams): Promise { - return this.request('/grid_weapons/resolve', { - method: 'POST', - body: params - }) - } + async resolveWeaponConflict(params: ResolveConflictParams, headers?: Record): Promise { + return this.request('/grid_weapons/resolve', { + method: 'POST', + body: { resolve: params }, + headers + }) + } /** * Updates weapon position */ - async updateWeaponPosition(params: UpdatePositionParams): Promise { - const { partyId, id, ...positionData } = params - return this.request(`/parties/${partyId}/grid_weapons/${id}/position`, { - method: 'PUT', - body: positionData - }) - } + async updateWeaponPosition(params: UpdatePositionParams, headers?: Record): Promise { + const { id, position, container, partyId } = params + return this.request(`/parties/${partyId}/grid_weapons/${id}/position`, { + method: 'PUT', + body: { position, container }, + headers + }) + } /** * Swaps two weapon positions */ - async swapWeapons(params: SwapPositionsParams): Promise<{ - source: GridWeapon - target: GridWeapon - }> { - const { partyId, ...swapData } = params - return this.request(`/parties/${partyId}/grid_weapons/swap`, { - method: 'POST', - body: swapData - }) - } + async swapWeapons(params: SwapPositionsParams, headers?: Record): Promise<{ + source: GridWeapon + target: GridWeapon + }> { + const { partyId, sourceId, targetId } = params + return this.request(`/parties/${partyId}/grid_weapons/swap`, { + method: 'POST', + body: { source_id: sourceId, target_id: targetId }, + headers + }) + } // Character operations /** * Creates a new grid character instance */ - async createCharacter(params: CreateGridCharacterParams): Promise { - return this.request('/grid_characters', { - method: 'POST', - body: params - }) - } + async createCharacter(params: CreateGridCharacterParams, headers?: Record): Promise { + return this.request('/grid_characters', { + method: 'POST', + body: { character: params }, + headers + }) + } /** * Updates a grid character instance */ - async updateCharacter(id: string, params: Partial): Promise { - return this.request(`/grid_characters/${id}`, { - method: 'PUT', - body: params - }) - } + async updateCharacter(id: string, params: Partial, headers?: Record): Promise { + return this.request(`/grid_characters/${id}`, { + method: 'PUT', + body: { character: params }, + headers + }) + } /** * Deletes a grid character instance */ - async deleteCharacter(params: { id?: string; partyId: string; position?: number }): Promise { - return this.request('/grid_characters', { - method: 'DELETE', - body: params - }) - } + async deleteCharacter(params: { id?: string; partyId: string; position?: number }, headers?: Record): Promise { + // If we have an ID, use it in the URL (standard Rails REST) + if (params.id) { + return this.request(`/grid_characters/${params.id}`, { + method: 'DELETE', + headers + }) + } + // Otherwise, send params in body for position-based delete + return this.request('/grid_characters/delete_by_position', { + method: 'DELETE', + body: params, + headers + }) + } /** * Updates character uncap level */ - async updateCharacterUncap(params: UpdateUncapParams): Promise { - return this.request('/grid_characters/update_uncap', { - method: 'POST', - body: params - }) - } + async updateCharacterUncap(params: UpdateUncapParams, headers?: Record): Promise { + return this.request('/grid_characters/update_uncap', { + method: 'POST', + body: { + character: { + id: params.id, + partyId: params.partyId, + uncapLevel: params.uncapLevel, + transcendenceStep: params.transcendenceStep + } + }, + headers + }) + } /** * Resolves character conflicts */ - async resolveCharacterConflict(params: ResolveConflictParams): Promise { - return this.request('/grid_characters/resolve', { - method: 'POST', - body: params - }) - } + async resolveCharacterConflict(params: ResolveConflictParams, headers?: Record): Promise { + return this.request('/grid_characters/resolve', { + method: 'POST', + body: { resolve: params }, + headers + }) + } /** * Updates character position */ - async updateCharacterPosition(params: UpdatePositionParams): Promise { - const { partyId, id, ...positionData } = params - return this.request(`/parties/${partyId}/grid_characters/${id}/position`, { - method: 'PUT', - body: positionData - }) - } + async updateCharacterPosition(params: UpdatePositionParams, headers?: Record): Promise { + const { id, position, container, partyId } = params + return this.request(`/parties/${partyId}/grid_characters/${id}/position`, { + method: 'PUT', + body: { position, container }, + headers + }) + } /** * Swaps two character positions */ - async swapCharacters(params: SwapPositionsParams): Promise<{ - source: GridCharacter - target: GridCharacter - }> { - const { partyId, ...swapData } = params - return this.request(`/parties/${partyId}/grid_characters/swap`, { - method: 'POST', - body: swapData - }) - } + async swapCharacters(params: SwapPositionsParams, headers?: Record): Promise<{ + source: GridCharacter + target: GridCharacter + }> { + const { partyId, sourceId, targetId } = params + return this.request(`/parties/${partyId}/grid_characters/swap`, { + method: 'POST', + body: { source_id: sourceId, target_id: targetId }, + headers + }) + } // Summon operations /** * Creates a new grid summon instance */ - async createSummon(params: CreateGridSummonParams): Promise { - return this.request('/grid_summons', { - method: 'POST', - body: params - }) - } + async createSummon(params: CreateGridSummonParams, headers?: Record): Promise { + return this.request('/grid_summons', { + method: 'POST', + body: { summon: params }, + headers + }) + } /** * Updates a grid summon instance */ - async updateSummon(id: string, params: Partial): Promise { - return this.request(`/grid_summons/${id}`, { - method: 'PUT', - body: params - }) - } + async updateSummon(id: string, params: Partial, headers?: Record): Promise { + return this.request(`/grid_summons/${id}`, { + method: 'PUT', + body: { summon: params }, + headers + }) + } /** * Deletes a grid summon instance */ - async deleteSummon(params: { id?: string; partyId: string; position?: number }): Promise { - return this.request('/grid_summons', { - method: 'DELETE', - body: params - }) - } + async deleteSummon(params: { id?: string; partyId: string; position?: number }, headers?: Record): Promise { + // If we have an ID, use it in the URL (standard Rails REST) + if (params.id) { + return this.request(`/grid_summons/${params.id}`, { + method: 'DELETE', + headers + }) + } + // Otherwise, send params in body for position-based delete + return this.request('/grid_summons/delete_by_position', { + method: 'DELETE', + body: params, + headers + }) + } /** * Updates summon uncap level */ - async updateSummonUncap(params: UpdateUncapParams): Promise { - return this.request('/grid_summons/update_uncap', { - method: 'POST', - body: params - }) - } + async updateSummonUncap(params: UpdateUncapParams, headers?: Record): Promise { + return this.request('/grid_summons/update_uncap', { + method: 'POST', + body: { + summon: { + id: params.id, + partyId: params.partyId, + uncapLevel: params.uncapLevel, + transcendenceStep: params.transcendenceStep + } + }, + headers + }) + } /** * Updates summon quick summon setting @@ -359,27 +422,29 @@ export class GridAdapter extends BaseAdapter { /** * Updates summon position */ - async updateSummonPosition(params: UpdatePositionParams): Promise { - const { partyId, id, ...positionData } = params - return this.request(`/parties/${partyId}/grid_summons/${id}/position`, { - method: 'PUT', - body: positionData - }) - } + async updateSummonPosition(params: UpdatePositionParams, headers?: Record): Promise { + const { id, position, container, partyId } = params + return this.request(`/parties/${partyId}/grid_summons/${id}/position`, { + method: 'PUT', + body: { position, container }, + headers + }) + } /** * Swaps two summon positions */ - async swapSummons(params: SwapPositionsParams): Promise<{ - source: GridSummon - target: GridSummon - }> { - const { partyId, ...swapData } = params - return this.request(`/parties/${partyId}/grid_summons/swap`, { - method: 'POST', - body: swapData - }) - } + async swapSummons(params: SwapPositionsParams, headers?: Record): Promise<{ + source: GridSummon + target: GridSummon + }> { + const { partyId, sourceId, targetId } = params + return this.request(`/parties/${partyId}/grid_summons/swap`, { + method: 'POST', + body: { source_id: sourceId, target_id: targetId }, + headers + }) + } /** * Clears grid-specific cache @@ -396,4 +461,4 @@ export class GridAdapter extends BaseAdapter { /** * Default grid adapter instance */ -export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file +export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG) diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts index 31e1fc58..8fdf8fa8 100644 --- a/src/lib/api/adapters/party.adapter.ts +++ b/src/lib/api/adapters/party.adapter.ts @@ -196,6 +196,8 @@ export interface GridUpdateResponse { export class PartyAdapter extends BaseAdapter { constructor(options?: AdapterOptions) { super(options) + // Temporarily disable cache until cache invalidation is fixed + this.disableCache = true } /** diff --git a/src/lib/api/adapters/user.adapter.ts b/src/lib/api/adapters/user.adapter.ts index c8d1e772..dc201038 100644 --- a/src/lib/api/adapters/user.adapter.ts +++ b/src/lib/api/adapters/user.adapter.ts @@ -1,5 +1,6 @@ import { BaseAdapter } from './base.adapter' import type { Party } from '$lib/types/api/party' +import type { RequestOptions } from './types' import { DEFAULT_ADAPTER_CONFIG } from './config' export interface UserInfo { @@ -36,8 +37,8 @@ export class UserAdapter extends BaseAdapter { /** * Get user information */ - async getInfo(username: string): Promise { - return this.request(`/users/info/${encodeURIComponent(username)}`) + async getInfo(username: string, options?: RequestOptions): Promise { + return this.request(`/users/info/${encodeURIComponent(username)}`, options) } /**