# API Refactoring Plan: Server-Side Only Architecture ## Executive Summary This document outlines the plan to refactor all Rails API calls in the hensei-svelte application to be server-side only, resolving authentication issues and improving security. ## Current Architecture Analysis ### API Layer (`/lib/api/`) - **`core.ts`**: Basic fetch wrappers that accept a `FetchLike` function - **`resources/`**: API endpoint functions (parties, grid, search, etc.) - Functions accept `fetch` as a parameter to work both client and server-side ### Service Layer (`/lib/services/`) - `PartyService`, `GridService`, `ConflictService` - Services accept `fetch` in constructor and use API resources - Currently used both client-side and server-side ### Authentication - `hooks.server.ts` has `handleFetch` that adds Bearer token to server-side requests - Client-side calls don't have access to the httpOnly auth token - Edit keys for anonymous users are stored in localStorage (client-side) - This is causing the 401 Unauthorized errors ## Problems with Current Approach 1. **Direct client API calls bypass authentication** - The `/teams/new/+page.svelte` directly imports and uses `gridApi` functions 2. **Security issue** - Client shouldn't directly call backend API 3. **Inconsistent authentication** - Only server-side fetch has the Bearer token 4. **Edit keys are client-side only** - Server can't access localStorage where edit keys are stored ## Proposed Solution: Server-Side API Proxy Routes ### Step 1: Create Generic API Proxy Routes Create server endpoints that mirror the Rails API structure: #### `/src/routes/api/parties/+server.ts` - POST: Create new party - Handles both authenticated and anonymous users #### `/src/routes/api/parties/[id]/+server.ts` - PUT: Update party details - DELETE: Delete party - Validates edit permissions (authenticated user or edit key) #### `/src/routes/api/parties/[id]/weapons/+server.ts` - POST: Add weapon to party - PUT: Update weapon in party - DELETE: Remove weapon from party #### `/src/routes/api/parties/[id]/summons/+server.ts` - POST: Add summon to party - PUT: Update summon in party - DELETE: Remove summon from party #### `/src/routes/api/parties/[id]/characters/+server.ts` - POST: Add character to party - PUT: Update character in party - DELETE: Remove character from party ### Step 2: Handle Edit Keys Properly Since edit keys are in localStorage (client-side), we need to: 1. Pass edit key as a header from client to our proxy endpoints 2. Server proxy validates and forwards it to Rails API 3. Structure: Client → SvelteKit Server (with edit key) → Rails API Example flow: ```javascript // Client-side const editKey = localStorage.getItem(`edit_key_${party.shortcode}`) await fetch('/api/parties/123/weapons', { method: 'POST', headers: { 'X-Edit-Key': editKey, // Pass to our server 'Content-Type': 'application/json' }, body: JSON.stringify({...}) }) // Server-side proxy export async function POST({ request, params, fetch, locals }) { const editKey = request.headers.get('X-Edit-Key') const body = await request.json() // Server's fetch automatically adds Bearer token via handleFetch const response = await fetch(`${API_BASE}/weapons`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(editKey ? { 'X-Edit-Key': editKey } : {}) }, body: JSON.stringify(body) }) return response } ``` ### Step 3: Create a Unified API Client Create `/src/lib/api/client.ts` that: - Works only on client-side - Automatically includes edit keys from localStorage - Calls our SvelteKit proxy endpoints (not Rails directly) - Can be used in both `/teams/new` and `/teams/[shortcode]` ```typescript export class APIClient { private getEditKey(partyId: string): string | null { if (typeof window === 'undefined') return null return localStorage.getItem(`edit_key_${partyId}`) } async addWeapon(partyId: string, weaponId: string, position: number, options?: any) { const editKey = this.getEditKey(partyId) const response = await fetch(`/api/parties/${partyId}/weapons`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(editKey ? { 'X-Edit-Key': editKey } : {}) }, body: JSON.stringify({ weaponId, position, ...options }) }) if (!response.ok) throw new Error(`Failed to add weapon: ${response.statusText}`) return response.json() } // Similar methods for other operations... } ``` ### Step 4: Update Components #### `/routes/teams/new/+page.svelte` - Remove direct `gridApi` and `partiesApi` imports - Use the new `APIClient` class - Edit keys handled automatically by the client #### `/routes/teams/[shortcode]/+page.svelte` (via Party component) - Use the same `APIClient` for consistency - Edit keys retrieved from localStorage when needed #### Services (`PartyService`, `GridService`) - Keep them server-side only - Used in `+page.server.ts` files - Client components use `APIClient` instead ### Step 5: Authentication Flow #### Authenticated Users 1. Client → SvelteKit Server (no edit key needed) 2. SvelteKit Server → Rails API (with Bearer token from cookies) #### Anonymous Users 1. Client → SvelteKit Server (with edit key from localStorage) 2. SvelteKit Server → Rails API (with edit key header) ## Benefits of This Approach 1. **Single API interface** - Same `APIClient` works for both new and existing parties 2. **Proper authentication** - Server-side requests include Bearer token 3. **Edit key support** - Anonymous users can still edit their parties 4. **Security** - Rails API never exposed to client 5. **Reusability** - Same code paths for `/teams/new` and `/teams/[shortcode]` 6. **Progressive enhancement** - Can still use form actions where appropriate ## Implementation Order 1. Create the API proxy routes in `/src/routes/api/` 2. Create the `APIClient` class 3. Update `/routes/teams/new/+page.svelte` to use `APIClient` 4. Update Party component to use `APIClient` for grid operations 5. Test both authenticated and anonymous user flows 6. Remove direct API imports from client components ## Testing Checklist ### Authenticated User Flow - [ ] Can create new party - [ ] Can add weapons/summons/characters to new party - [ ] Can edit existing party they own - [ ] Cannot edit party they don't own ### Anonymous User Flow - [ ] Can create new party (receives edit key) - [ ] Can add items to party using edit key - [ ] Can edit party after page refresh (edit key persists) - [ ] Cannot edit without valid edit key ### Error Handling - [ ] 401 errors properly handled - [ ] Network errors display user-friendly messages - [ ] Invalid data errors show validation messages ## Migration Path This refactor can be done incrementally: 1. Start with new proxy routes (doesn't break existing code) 2. Update one component at a time to use new API client 3. Gradually remove direct API imports 4. Finally remove unused code ## Notes - The `account` cookie is httpOnly for security, which is why we need server-side proxy - Edit keys must be passed from client since they're in localStorage - All Rails API endpoints should remain unchanged - This architecture follows SvelteKit best practices for API integration