hensei-web/docs/api-refactor-plan.md

7.1 KiB

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:

// 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]
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