211 lines
No EOL
7.1 KiB
Markdown
211 lines
No EOL
7.1 KiB
Markdown
# 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 |