managarten/maerchenzauber/apps/backend/RLS_IMPLEMENTATION_GUIDE.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

195 lines
No EOL
6.2 KiB
Markdown

# RLS Implementation Guide for Maerchenzauber Storyteller
## Overview
This document describes the Row Level Security (RLS) implementation for the Maerchenzauber Storyteller project, based on proven patterns from the Memoro project. The implementation provides secure, user-based access control for both database tables and storage buckets.
## Implemented Changes
### 1. Core Authentication Functions
We've created four essential authentication helper functions:
#### `current_user_id()`
- **Purpose**: Extracts user identity from JWT claims
- **Returns**: User ID as text
- **Usage**: Primary function for identifying the current user in RLS policies
#### `auth_is_admin()`
- **Purpose**: Checks if the current user has admin privileges
- **Returns**: Boolean (true if admin, false otherwise)
- **Usage**: Used in policies that require admin access (creators, errors tables)
#### `current_user_uuid()`
- **Purpose**: Returns the current user's ID as UUID
- **Returns**: UUID
- **Usage**: Compatibility function when UUID type is needed
#### `user_owns_resource(resource_user_id text)`
- **Purpose**: Helper function to check resource ownership
- **Returns**: Boolean
- **Usage**: Simplifies ownership checks in policies
### 2. Table RLS Policies
#### Characters Table
- **View**: Users can only see their own characters
- **Insert**: Users can only create characters with their user_id
- **Update**: Users can only update their own characters
- **Delete**: Users can only delete their own characters
- **Service Role**: Full access for backend operations
#### Stories Table
- **View**: Users can only see their own stories
- **Insert**: Users can only create stories with their user_id
- **Update**: Users can only update their own stories
- **Delete**: Users can only delete their own stories
- **Service Role**: Full access for backend operations
#### Creators Table
- **View**: All authenticated users can view creators
- **Insert/Update/Delete**: Only admins can modify creators
- **Service Role**: Full access for backend operations
#### Errors Table
- **View**: Only admins can view errors
- **Insert**: Authenticated users can insert their own errors (for logging)
- **Update/Delete**: Only admins can modify errors
- **Service Role**: Full access for backend operations
### 3. Storage RLS Policies
#### storyteller-images bucket (public)
- **Structure**: `{user_id}/stories/{story_id}/` or `{user_id}/characters/{character_id}/`
- **View**: Anyone can view images (public bucket)
- **Upload/Update/Delete**: Users can only manage files in their own folder (first folder must match their user_id)
- **Admin Access**: Admins can view and delete any file
#### user-uploads bucket (private)
- **Structure**: `{user_id}/{filename}`
- **View/Upload/Update/Delete**: Users can only access their own folder
- **Admin Access**: Admins can view and delete any file
### 4. Migration Files Created
1. **auth-functions.sql**: Core authentication functions
2. **update-rls-policies.sql**: Updated RLS policies using new functions
3. **storage-rls-policies.sql**: Storage bucket policies
4. **cleanup-duplicate-policies.sql**: Removed duplicate policies
5. **test-rls-policies.sql**: Comprehensive test queries
## Usage in Application Code
### TypeScript/JavaScript Example
```typescript
// File upload with proper path structure
const uploadFile = async (file: File, userId: string, storyId: string) => {
const filePath = `${userId}/stories/${storyId}/${Date.now()}_${file.name}`;
const { data, error } = await supabase.storage
.from('storyteller-images')
.upload(filePath, file);
if (error) throw error;
return data;
};
// Fetching user's own data (RLS automatically filters)
const getUserCharacters = async () => {
const { data, error } = await supabase
.from('characters')
.select('*');
// Only returns characters where user_id matches current user
return data;
};
```
### Admin Operations
```typescript
// Check if user is admin
const checkAdminStatus = async () => {
const { data, error } = await supabase
.rpc('auth_is_admin');
return data; // true or false
};
// Admin creating a creator (requires admin role)
const createCreator = async (creatorData: CreatorInput) => {
const { data, error } = await supabase
.from('creators')
.insert(creatorData);
// Will fail if user is not admin
return data;
};
```
## Security Best Practices
1. **Always use auth functions**: Use `current_user_id()` instead of direct JWT manipulation
2. **Test policies thoroughly**: Use the test-rls-policies.sql file to verify access
3. **Service role key protection**: Never expose service role key in client code
4. **Consistent user identification**: Always store user_id as text for compatibility
5. **Storage path structure**: Follow the defined folder structure for storage
## Troubleshooting
### Common Issues
1. **Access Denied Errors**
- Verify user is authenticated
- Check if user_id matches the resource owner
- Ensure RLS is enabled on the table
2. **Admin Functions Not Working**
- Verify user has admin flag in users table
- Check JWT claims include proper app_access data
- Ensure auth_is_admin() function is working correctly
3. **Storage Upload Failures**
- Verify file path follows the required structure
- Check bucket exists and RLS is enabled
- Ensure user is authenticated
### Debug Queries
```sql
-- Check current user
SELECT current_user_id(), auth_is_admin();
-- View active policies
SELECT * FROM pg_policies WHERE tablename = 'your_table';
-- Test specific access
SET request.jwt.claim.sub = 'test-user-id';
SELECT * FROM characters;
```
## Maintenance
### Adding New Tables
When adding new tables with RLS:
1. Enable RLS: `ALTER TABLE new_table ENABLE ROW LEVEL SECURITY;`
2. Create policies using auth functions
3. Add service role policy
4. Test all CRUD operations
5. Document the policies
### Updating Policies
1. Always drop old policy before creating new one
2. Use migrations for version control
3. Test changes thoroughly
4. Update documentation
## References
- [Supabase RLS Documentation](https://supabase.com/docs/guides/auth/row-level-security)
- [PostgreSQL RLS Guide](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
- Original Memoro RLS Implementation Guide