managarten/maerchenzauber/apps/backend/docs/CHARACTER_SHARING_RLS.md
Till-JS e7f5f942f3 chore: initial commit - consolidate 4 projects into monorepo
Projects included:
- maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing)
- manacore (Expo mobile + SvelteKit web + Astro landing)
- manadeck (NestJS backend + Expo mobile + SvelteKit web)
- memoro (Expo mobile + SvelteKit web + Astro landing)

This commit preserves the current state before monorepo restructuring.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 23:38:24 +01:00

177 lines
5.5 KiB
Markdown

# Character Sharing with Service Account
## Overview
Character sharing uses **authenticated requests** where users must be logged in, but the backend uses the **service account** (service role key) to bypass RLS when fetching shared characters.
## Architecture
**Security Model:**
- ✅ User must be authenticated to view shared characters
- ✅ Backend uses service account to bypass RLS
- ✅ No special RLS policies needed
- ✅ All character data remains protected by default RLS
**Benefits:**
- Know who is accessing shared characters (analytics, abuse prevention)
- No public endpoints exposing database directly
- Simpler RLS policy management
- Users must be logged in to import (already required for credits)
## Required RLS Policies
### User-Owned Characters Policy
Standard RLS policy - users can only read their own characters:
```sql
-- Policy name: "Users can read their own characters"
CREATE POLICY "users_read_own_characters"
ON characters
FOR SELECT
USING (auth.uid() = user_id);
```
**Note:** No additional policies needed! The backend service account bypasses RLS entirely.
## Testing
### Test 1: Verify Authenticated Access to Shared Character
```bash
# Test authenticated user accessing a shared character
curl -X GET "http://localhost:3002/character/shared/{characterId}" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
# Expected: 200 OK with character data (any character ID)
```
### Test 2: Verify Unauthenticated Access is Blocked
```bash
# Test without auth token
curl -X GET "http://localhost:3002/character/shared/{characterId}" \
-H "Content-Type: application/json"
# Expected: 401 Unauthorized
```
### Test 3: Verify User-Owned Characters Still Work
```bash
# Test authenticated user accessing their own characters
curl -X GET "http://localhost:3002/character" \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json"
# Expected: 200 OK with list of user's characters
```
## Backend Changes Made
### 1. Added Admin Client to SupabaseProvider
**File**: `backend/src/supabase/supabase.provider.ts`
- Added `getAdminClient()` method
- Uses `MAERCHENZAUBER_SUPABASE_SERVICE_ROLE_KEY`
- Bypasses all RLS policies
### 2. Updated getSharedCharacter
**File**: `backend/src/core/services/supabase-jsonb-auth.service.ts`
- Changed from `getClient()` to `getAdminClient()`
- Uses service account for database access
- User still must be authenticated to call endpoint
### 3. CharacterController
**File**: `backend/src/character/character.controller.ts`
- Kept `@UseGuards(AuthGuard)` - requires authentication
- Added `@CurrentUser()` to track who accesses shared characters
## Mobile App Changes
### 1. Created Deeplink Route
**File**: `mobile/app/share/character/[id].tsx`
- Handles `maerchenzauber://share/character/{id}` deeplinks
- Redirects to character preview page
- Added error handling to prevent crashes
### 2. Improved Error Handling
- Added try/catch blocks
- User-friendly error messages
- Graceful fallback to home screen on errors
## Deployment Steps
1. **Update Backend Code**
```bash
cd backend
npm run build
npm run start:prod
```
2. **Update RLS Policies in Supabase Dashboard**
- Go to Supabase Dashboard > Authentication > Policies
- Add the "public_read_shared_characters" policy
- Verify existing user policies remain active
3. **Deploy Backend to Cloud Run**
```bash
cd backend
gcloud builds submit --tag gcr.io/PROJECT_ID/storyteller-backend
gcloud run deploy storyteller-backend --image gcr.io/PROJECT_ID/storyteller-backend
```
4. **Build and Deploy Mobile App**
```bash
cd mobile
eas build --platform ios --profile production
eas submit --platform ios
```
## Verification Checklist
- [ ] Backend compiles without TypeScript errors
- [ ] Public endpoint is accessible without authentication
- [ ] Authenticated users can still access their own characters
- [ ] RLS policy allows reading shared characters
- [ ] RLS policy blocks reading private characters
- [ ] Deeplink opens app successfully
- [ ] Character preview loads without crashing
- [ ] Error handling works for invalid character IDs
- [ ] Import functionality still requires authentication (10 credits)
## Security Considerations
1. **Authentication Required**
- User must be logged in to view shared characters
- Prevents anonymous abuse
- Tracks who accesses what (analytics)
2. **Service Account Usage**
- Backend uses service role key to bypass RLS
- Only exposed through authenticated endpoints
- No direct database access from frontend
3. **Import Requirements**
- Still requires authentication
- Costs 10 credits per import
- Prevents importing own characters
- Checks for duplicate imports
4. **RLS Still Protects Data**
- Users can only modify their own characters
- Default RLS policies remain unchanged
- Service account only used for read operations on shared endpoint
## Troubleshooting
### Issue: "Character not found or not available for sharing"
- **Cause**: RLS policy not configured correctly
- **Fix**: Add the `public_read_shared_characters` policy
### Issue: "401 Unauthorized" when accessing shared endpoint
- **Cause**: `@Public()` decorator not applied
- **Fix**: Verify `PublicAuthGuard` is being used and `@Public()` is on endpoint
### Issue: App crashes when opening deeplink
- **Cause**: Missing mobile route file
- **Fix**: Ensure `mobile/app/share/character/[id].tsx` exists
### Issue: "Unmatched Route" error
- **Cause**: Expo Router can't find the route
- **Fix**: Restart Expo dev server with `npx expo start -c`