mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 04:31:09 -05:00
Refactor user account system and remove moderation integration
- Remove first_name and last_name fields from User model - Add user deletion and social provider services - Restructure auth serializers into separate directory - Update avatar upload functionality and API endpoints - Remove django-moderation integration documentation - Add mandatory compliance enforcement rules - Update frontend documentation with API usage examples
This commit is contained in:
107
docs/avatar-upload-debugging.md
Normal file
107
docs/avatar-upload-debugging.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Avatar Upload Debugging Guide
|
||||
|
||||
Last Updated: 2025-08-29
|
||||
|
||||
## Issue Summary
|
||||
|
||||
The avatar upload functionality was experiencing file corruption during transmission from the frontend to the backend. PNG files were being corrupted where the PNG header `\x89PNG` was being replaced with UTF-8 replacement characters `\xef\xbf\xbdPNG`.
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Next.js Proxy Issue (ACTUAL ROOT CAUSE)
|
||||
The primary issue was in the Next.js API proxy route that converts binary FormData to text:
|
||||
|
||||
**File:** `/Users/talor/dyad-apps/thrillwiki-real/src/app/api/[...path]/route.ts`
|
||||
|
||||
1. **Problematic code:**
|
||||
```typescript
|
||||
body = await clonedRequest.text(); // This corrupts binary data!
|
||||
```
|
||||
|
||||
2. **Why this causes corruption:**
|
||||
- FormData containing binary image files gets converted to UTF-8 text
|
||||
- Binary bytes like `\x89` (PNG signature) become UTF-8 replacement characters `\xef\xbf\xbd`
|
||||
- This mangles the file data before it even reaches the backend
|
||||
|
||||
|
||||
## Solutions Implemented
|
||||
|
||||
### Next.js Proxy Fix (PRIMARY FIX)
|
||||
**File:** `/Users/talor/dyad-apps/thrillwiki-real/src/app/api/[...path]/route.ts`
|
||||
|
||||
Fixed the proxy to preserve binary data instead of converting to text:
|
||||
|
||||
```typescript
|
||||
// BEFORE (problematic)
|
||||
body = await clonedRequest.text(); // Corrupts binary data!
|
||||
|
||||
// AFTER (fixed)
|
||||
const arrayBuffer = await clonedRequest.arrayBuffer();
|
||||
body = new Uint8Array(arrayBuffer); // Preserves binary data
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- `arrayBuffer()` preserves the original binary data
|
||||
- `Uint8Array` maintains the exact byte sequence
|
||||
- No UTF-8 text conversion that corrupts binary files
|
||||
|
||||
### Backend Cleanup
|
||||
**File:** `/Users/talor/thrillwiki_django_no_react/backend/apps/api/v1/serializers/accounts.py`
|
||||
|
||||
Simplified the avatar validation to basic file validation only:
|
||||
|
||||
```python
|
||||
# Removed complex corruption repair logic
|
||||
# Now just validates file size, format, and dimensions with PIL
|
||||
def validate_avatar(self, value):
|
||||
# Basic file validation only
|
||||
# Let Cloudflare Images handle the rest
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### API Flow
|
||||
1. **Frontend:** `AvatarUploader` → `useAuth.uploadAvatar()` → `accountApi.uploadAvatar(file)`
|
||||
2. **Next.js Proxy:** Preserves binary data using `arrayBuffer()` instead of `text()`
|
||||
3. **Backend:** `AvatarUploadView` → `AvatarUploadSerializer.validate_avatar()` → Cloudflare Images API
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Steps
|
||||
1. Select a PNG image file in the avatar uploader
|
||||
2. Upload the file
|
||||
3. Verify the upload succeeds without corruption
|
||||
4. Check that the avatar displays correctly
|
||||
|
||||
|
||||
## Prevention
|
||||
|
||||
### Frontend Best Practices
|
||||
- Always pass `FormData` objects directly to API calls
|
||||
- Avoid extracting and re-wrapping files unnecessarily
|
||||
- Use proper file validation on the client side
|
||||
|
||||
### Backend Best Practices
|
||||
- Use Django's proper file upload classes (`InMemoryUploadedFile`, `TemporaryUploadedFile`)
|
||||
- Implement robust file validation and corruption detection
|
||||
- Provide detailed logging for debugging file upload issues
|
||||
|
||||
## Related Files
|
||||
|
||||
### Frontend
|
||||
- `/Users/talor/dyad-apps/thrillwiki-real/src/components/profile/avatar-uploader.tsx`
|
||||
- `/Users/talor/dyad-apps/thrillwiki-real/src/hooks/use-auth.tsx`
|
||||
- `/Users/talor/dyad-apps/thrillwiki-real/src/lib/api.ts`
|
||||
|
||||
### Backend
|
||||
- `/Users/talor/thrillwiki_django_no_react/backend/apps/api/v1/serializers/accounts.py`
|
||||
- `/Users/talor/thrillwiki_django_no_react/backend/apps/api/v1/accounts/views.py`
|
||||
- `/Users/talor/thrillwiki_django_no_react/backend/apps/accounts/services.py`
|
||||
|
||||
## Status
|
||||
|
||||
✅ **RESOLVED** - File corruption issue fixed in Next.js proxy
|
||||
✅ **CLEANED UP** - Removed unnecessary corruption repair code from backend
|
||||
✅ **DOCUMENTED** - Comprehensive debugging guide created
|
||||
|
||||
The avatar upload functionality should now work correctly without file corruption. The Next.js proxy properly preserves binary data, and the backend performs basic file validation only.
|
||||
778
docs/frontend.md
778
docs/frontend.md
@@ -6,18 +6,33 @@ This document provides comprehensive documentation for all ThrillWiki API endpoi
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require authentication via JWT tokens. Include the token in the Authorization header:
|
||||
ThrillWiki uses JWT Bearer token authentication. After successful login or signup, you'll receive access and refresh tokens that must be included in subsequent API requests.
|
||||
|
||||
### Authentication Headers
|
||||
|
||||
Include the access token in the Authorization header using Bearer format:
|
||||
|
||||
```typescript
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
```
|
||||
|
||||
### Token Management
|
||||
|
||||
- **Access Token**: Short-lived token (1 hour) used for API requests
|
||||
- **Refresh Token**: Long-lived token (7 days) used to obtain new access tokens
|
||||
- **Token Rotation**: Refresh tokens are rotated on each refresh for enhanced security
|
||||
|
||||
## Base URL
|
||||
|
||||
All API endpoints are prefixed with `/api/v1/`
|
||||
The frontend uses a Next.js proxy that routes API requests:
|
||||
- Frontend requests: `/v1/auth/login/`, `/v1/accounts/profile/`, etc.
|
||||
- Proxy adds `/api/` prefix: `/api/v1/auth/login/`, `/api/v1/accounts/profile/`, etc.
|
||||
- Backend receives: `/api/v1/auth/login/`, `/api/v1/accounts/profile/`, etc.
|
||||
|
||||
**Important**: Frontend code should make requests to `/v1/...` endpoints, not `/api/v1/...`
|
||||
|
||||
## Moderation System API
|
||||
|
||||
@@ -271,19 +286,389 @@ The moderation system provides comprehensive content moderation, user management
|
||||
|
||||
### Login
|
||||
- **POST** `/api/v1/auth/login/`
|
||||
- **Body**: `{ "username": string, "password": string }`
|
||||
- **Body**: `{ "username": string, "password": string, "turnstile_token"?: string }`
|
||||
- **Returns**: JWT tokens and user data
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"access": string,
|
||||
"refresh": string,
|
||||
"user": {
|
||||
"id": number,
|
||||
"username": string,
|
||||
"email": string,
|
||||
"display_name": string,
|
||||
"is_active": boolean,
|
||||
"date_joined": string
|
||||
},
|
||||
"message": string
|
||||
}
|
||||
```
|
||||
|
||||
### Signup
|
||||
- **POST** `/api/v1/auth/signup/`
|
||||
- **Body**: User registration data
|
||||
- **Body**:
|
||||
```typescript
|
||||
{
|
||||
"username": string,
|
||||
"email": string,
|
||||
"password": string,
|
||||
"password_confirm": string,
|
||||
"display_name": string, // Required field
|
||||
"turnstile_token"?: string
|
||||
}
|
||||
```
|
||||
- **Returns**: JWT tokens and user data
|
||||
- **Response**: Same format as login response (access and refresh tokens)
|
||||
- **Note**: `display_name` is now required during registration. The system no longer uses separate first_name and last_name fields.
|
||||
|
||||
### Token Refresh
|
||||
- **POST** `/api/v1/auth/token/refresh/`
|
||||
- **Body**: `{ "refresh": string }`
|
||||
- **Returns**: New access token and optionally a new refresh token
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"access": string,
|
||||
"refresh"?: string // Only returned if refresh token rotation is enabled
|
||||
}
|
||||
```
|
||||
|
||||
### Social Authentication
|
||||
|
||||
#### Google Login
|
||||
- **POST** `/api/v1/auth/social/google/`
|
||||
- **Body**: `{ "access_token": string }`
|
||||
- **Returns**: JWT tokens and user data (same format as regular login)
|
||||
- **Note**: The access_token should be obtained from Google OAuth flow
|
||||
|
||||
#### Discord Login
|
||||
- **POST** `/api/v1/auth/social/discord/`
|
||||
- **Body**: `{ "access_token": string }`
|
||||
- **Returns**: JWT tokens and user data (same format as regular login)
|
||||
- **Note**: The access_token should be obtained from Discord OAuth flow
|
||||
|
||||
#### Connect Social Account
|
||||
- **POST** `/api/v1/auth/social/{provider}/connect/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Body**: `{ "access_token": string }`
|
||||
- **Returns**: `{ "success": boolean, "message": string }`
|
||||
- **Note**: Links a social account to an existing ThrillWiki account
|
||||
|
||||
#### Disconnect Social Account
|
||||
- **POST** `/api/v1/auth/social/{provider}/disconnect/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: `{ "success": boolean, "message": string }`
|
||||
|
||||
#### Get Social Connections
|
||||
- **GET** `/api/v1/auth/social/connections/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**:
|
||||
```typescript
|
||||
{
|
||||
"google": { "connected": boolean, "email"?: string },
|
||||
"discord": { "connected": boolean, "username"?: string }
|
||||
}
|
||||
```
|
||||
|
||||
### Social Provider Management
|
||||
|
||||
#### List Available Providers
|
||||
- **GET** `/api/v1/auth/social/providers/available/`
|
||||
- **Permissions**: Public access
|
||||
- **Returns**: List of available social providers for connection
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"available_providers": [
|
||||
{
|
||||
"id": "google",
|
||||
"name": "Google",
|
||||
"auth_url": "https://example.com/accounts/google/login/",
|
||||
"connect_url": "https://example.com/api/v1/auth/social/connect/google/"
|
||||
}
|
||||
],
|
||||
"count": number
|
||||
}
|
||||
```
|
||||
|
||||
#### List Connected Providers
|
||||
- **GET** `/api/v1/auth/social/connected/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: List of social providers connected to user's account
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"connected_providers": [
|
||||
{
|
||||
"provider": "google",
|
||||
"provider_name": "Google",
|
||||
"uid": "user_id_on_provider",
|
||||
"date_joined": "2025-01-01T00:00:00Z",
|
||||
"can_disconnect": boolean,
|
||||
"disconnect_reason": string | null,
|
||||
"extra_data": object
|
||||
}
|
||||
],
|
||||
"count": number,
|
||||
"has_password_auth": boolean,
|
||||
"can_disconnect_any": boolean
|
||||
}
|
||||
```
|
||||
|
||||
#### Connect Social Provider
|
||||
- **POST** `/api/v1/auth/social/connect/{provider}/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Parameters**: `provider` - Provider ID (e.g., 'google', 'discord')
|
||||
- **Returns**: Connection initiation response with auth URL
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"success": boolean,
|
||||
"message": string,
|
||||
"provider": string,
|
||||
"auth_url": string
|
||||
}
|
||||
```
|
||||
- **Error Responses**:
|
||||
- `400`: Provider already connected or invalid provider
|
||||
- `500`: Connection initiation failed
|
||||
|
||||
#### Disconnect Social Provider
|
||||
- **DELETE** `/api/v1/auth/social/disconnect/{provider}/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Parameters**: `provider` - Provider ID to disconnect
|
||||
- **Returns**: Disconnection result with safety information
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"success": boolean,
|
||||
"message": string,
|
||||
"provider": string,
|
||||
"remaining_providers": string[],
|
||||
"has_password_auth": boolean,
|
||||
"suggestions"?: string[]
|
||||
}
|
||||
```
|
||||
- **Error Responses**:
|
||||
- `400`: Cannot disconnect (safety validation failed)
|
||||
- `404`: Provider not connected
|
||||
- `500`: Disconnection failed
|
||||
|
||||
#### Get Social Authentication Status
|
||||
- **GET** `/api/v1/auth/social/status/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: Comprehensive social authentication status
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"user_id": number,
|
||||
"username": string,
|
||||
"email": string,
|
||||
"has_password_auth": boolean,
|
||||
"connected_providers": ConnectedProvider[],
|
||||
"total_auth_methods": number,
|
||||
"can_disconnect_any": boolean,
|
||||
"requires_password_setup": boolean
|
||||
}
|
||||
```
|
||||
|
||||
### Social Provider Safety Rules
|
||||
|
||||
The social provider management system enforces strict safety rules to prevent users from locking themselves out:
|
||||
|
||||
1. **Disconnection Safety**: Users can only disconnect a social provider if they have:
|
||||
- Another social provider connected, OR
|
||||
- Email + password authentication set up
|
||||
|
||||
2. **Error Scenarios**:
|
||||
- **Only Provider + No Password**: "Cannot disconnect your only authentication method. Please set up a password or connect another social provider first."
|
||||
- **No Password Auth**: "Please set up email/password authentication before disconnecting this provider."
|
||||
|
||||
3. **Suggested Actions**: When disconnection is blocked, the API provides suggestions:
|
||||
- "Set up password authentication"
|
||||
- "Connect another social provider"
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Check Social Provider Status
|
||||
```typescript
|
||||
const checkSocialStatus = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/social/status/', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.requires_password_setup) {
|
||||
// Show password setup prompt
|
||||
showPasswordSetupModal();
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get social status:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Connect Social Provider
|
||||
```typescript
|
||||
const connectProvider = async (provider: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/auth/social/connect/${provider}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Redirect to provider auth URL
|
||||
window.location.href = data.auth_url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect provider:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Disconnect Social Provider with Safety Check
|
||||
```typescript
|
||||
const disconnectProvider = async (provider: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/auth/social/disconnect/${provider}/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 400) {
|
||||
// Show safety warning with suggestions
|
||||
showSafetyWarning(data.error, data.suggestions);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Success - update UI
|
||||
toast.success(data.message);
|
||||
refreshConnectedProviders();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to disconnect provider:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### React Component Example
|
||||
```typescript
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface SocialProvider {
|
||||
provider: string;
|
||||
provider_name: string;
|
||||
can_disconnect: boolean;
|
||||
disconnect_reason?: string;
|
||||
}
|
||||
|
||||
const SocialProviderManager: React.FC = () => {
|
||||
const [connectedProviders, setConnectedProviders] = useState<SocialProvider[]>([]);
|
||||
const [hasPasswordAuth, setHasPasswordAuth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadConnectedProviders();
|
||||
}, []);
|
||||
|
||||
const loadConnectedProviders = async () => {
|
||||
const response = await fetch('/api/v1/auth/social/connected/', {
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
setConnectedProviders(data.connected_providers);
|
||||
setHasPasswordAuth(data.has_password_auth);
|
||||
};
|
||||
|
||||
const handleDisconnect = async (provider: string) => {
|
||||
const response = await fetch(`/api/v1/auth/social/disconnect/${provider}/`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
alert(error.error + '\n\nSuggestions:\n' + error.suggestions?.join('\n'));
|
||||
return;
|
||||
}
|
||||
|
||||
loadConnectedProviders(); // Refresh list
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3>Connected Social Accounts</h3>
|
||||
|
||||
{!hasPasswordAuth && connectedProviders.length === 1 && (
|
||||
<div className="bg-yellow-50 p-4 rounded-lg">
|
||||
<p className="text-yellow-800">
|
||||
⚠️ Set up a password to safely manage your social connections
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectedProviders.map((provider) => (
|
||||
<div key={provider.provider} className="flex items-center justify-between p-4 border rounded">
|
||||
<span>{provider.provider_name}</span>
|
||||
|
||||
<button
|
||||
onClick={() => handleDisconnect(provider.provider)}
|
||||
disabled={!provider.can_disconnect}
|
||||
className={`px-4 py-2 rounded ${
|
||||
provider.can_disconnect
|
||||
? 'bg-red-600 text-white hover:bg-red-700'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
title={provider.disconnect_reason}
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Logout
|
||||
- **POST** `/api/v1/auth/logout/`
|
||||
- **Returns**: `{ "message": string }`
|
||||
|
||||
### Current User
|
||||
- **GET** `/api/v1/auth/user/`
|
||||
- **Returns**: Current user profile data
|
||||
- **Response**:
|
||||
```typescript
|
||||
{
|
||||
"id": number,
|
||||
"username": string,
|
||||
"email": string,
|
||||
"display_name": string,
|
||||
"is_active": boolean,
|
||||
"date_joined": string
|
||||
}
|
||||
```
|
||||
|
||||
### Password Reset
|
||||
- **POST** `/api/v1/auth/password/reset/`
|
||||
@@ -293,6 +678,121 @@ The moderation system provides comprehensive content moderation, user management
|
||||
- **POST** `/api/v1/auth/password/change/`
|
||||
- **Body**: `{ "old_password": string, "new_password": string }`
|
||||
|
||||
## User Account Management API
|
||||
|
||||
### User Profile
|
||||
- **GET** `/api/v1/accounts/profile/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: Complete user profile including account details, preferences, and statistics
|
||||
|
||||
### Update Account
|
||||
- **PATCH** `/api/v1/accounts/profile/account/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Body**: `{ "display_name"?: string, "email"?: string }`
|
||||
|
||||
### Update Profile
|
||||
- **PATCH** `/api/v1/accounts/profile/update/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Body**: `{ "display_name"?: string, "pronouns"?: string, "bio"?: string, "twitter"?: string, "instagram"?: string, "youtube"?: string, "discord"?: string }`
|
||||
|
||||
### Avatar Upload
|
||||
- **POST** `/api/v1/accounts/profile/avatar/upload/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Content-Type**: `multipart/form-data`
|
||||
- **Body**: FormData with `avatar` field containing image file (JPEG, PNG, WebP)
|
||||
- **Returns**:
|
||||
```typescript
|
||||
{
|
||||
"success": boolean,
|
||||
"message": string,
|
||||
"avatar_url": string,
|
||||
"avatar_variants": {
|
||||
"thumbnail": string, // 64x64
|
||||
"avatar": string, // 200x200
|
||||
"large": string // 400x400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL AUTHENTICATION REQUIREMENT**:
|
||||
- This endpoint requires authentication via JWT token in Authorization header
|
||||
- **Common Issue**: "Authentication credentials were not provided" (401 error)
|
||||
- **Root Cause**: User not logged in or JWT token not being sent
|
||||
- **Debug Steps**: See `docs/avatar-upload-debugging.md` for comprehensive troubleshooting guide
|
||||
|
||||
**Usage Example**:
|
||||
```typescript
|
||||
// Ensure user is logged in first
|
||||
const { user } = useAuth();
|
||||
if (!user) {
|
||||
// Redirect to login
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload avatar
|
||||
const file = event.target.files[0];
|
||||
const response = await accountApi.uploadAvatar(file);
|
||||
```
|
||||
|
||||
**Test Credentials** (for debugging):
|
||||
- Username: `testuser`
|
||||
- Password: `testpass123`
|
||||
- Email: `test@example.com`
|
||||
|
||||
**Recent Fix (2025-08-29)**:
|
||||
- Fixed file corruption issue where PNG headers were being corrupted during upload
|
||||
- Frontend now passes FormData directly instead of extracting and re-wrapping files
|
||||
- Backend includes corruption detection and repair mechanism
|
||||
- See `docs/avatar-upload-debugging.md` for complete technical details
|
||||
|
||||
### Avatar Delete
|
||||
- **DELETE** `/api/v1/accounts/profile/avatar/delete/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: `{ "success": boolean, "message": string, "avatar_url": string }`
|
||||
|
||||
### User Preferences
|
||||
- **GET** `/api/v1/accounts/preferences/`
|
||||
- **PATCH** `/api/v1/accounts/preferences/update/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### Notification Settings
|
||||
- **GET** `/api/v1/accounts/settings/notifications/`
|
||||
- **PATCH** `/api/v1/accounts/settings/notifications/update/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### Privacy Settings
|
||||
- **GET** `/api/v1/accounts/settings/privacy/`
|
||||
- **PATCH** `/api/v1/accounts/settings/privacy/update/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### Security Settings
|
||||
- **GET** `/api/v1/accounts/settings/security/`
|
||||
- **PATCH** `/api/v1/accounts/settings/security/update/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### User Statistics
|
||||
- **GET** `/api/v1/accounts/statistics/`
|
||||
- **Permissions**: Authenticated users only
|
||||
- **Returns**: User activity statistics, ride credits, contributions, and achievements
|
||||
|
||||
### Top Lists
|
||||
- **GET** `/api/v1/accounts/top-lists/`
|
||||
- **POST** `/api/v1/accounts/top-lists/create/`
|
||||
- **PATCH** `/api/v1/accounts/top-lists/{id}/`
|
||||
- **DELETE** `/api/v1/accounts/top-lists/{id}/delete/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### Notifications
|
||||
- **GET** `/api/v1/accounts/notifications/`
|
||||
- **PATCH** `/api/v1/accounts/notifications/mark-read/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
### Account Deletion
|
||||
- **POST** `/api/v1/accounts/delete-account/request/`
|
||||
- **POST** `/api/v1/accounts/delete-account/verify/`
|
||||
- **POST** `/api/v1/accounts/delete-account/cancel/`
|
||||
- **Permissions**: Authenticated users only
|
||||
|
||||
## Statistics API
|
||||
|
||||
### Global Statistics
|
||||
@@ -314,7 +814,9 @@ The moderation system provides comprehensive content moderation, user management
|
||||
|
||||
## Error Handling
|
||||
|
||||
All API endpoints return standardized error responses:
|
||||
All API endpoints return standardized error responses. The system provides enhanced error handling with detailed messages, error codes, and contextual information.
|
||||
|
||||
### Standard Error Response Format
|
||||
|
||||
```typescript
|
||||
interface ApiError {
|
||||
@@ -329,12 +831,274 @@ interface ApiError {
|
||||
}
|
||||
```
|
||||
|
||||
Common error codes:
|
||||
### Enhanced Error Response Format
|
||||
|
||||
For critical operations like account deletion, the API returns enhanced error responses with additional context:
|
||||
|
||||
```typescript
|
||||
interface EnhancedApiError {
|
||||
status: "error";
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
error_code: string;
|
||||
user_info?: {
|
||||
username: string;
|
||||
role: string;
|
||||
is_superuser: boolean;
|
||||
is_staff: boolean;
|
||||
};
|
||||
help_text?: string;
|
||||
details?: any;
|
||||
request_user?: string;
|
||||
};
|
||||
data: null;
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling in React Components
|
||||
|
||||
Here's how to handle and display enhanced error messages in your NextJS components:
|
||||
|
||||
```typescript
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
interface ErrorDisplayProps {
|
||||
error: EnhancedApiError | null;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onDismiss }) => {
|
||||
if (!error) return null;
|
||||
|
||||
const { error: errorData } = error;
|
||||
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{errorData.message}
|
||||
</h3>
|
||||
|
||||
{errorData.error_code && (
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
Error Code: {errorData.error_code}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{errorData.user_info && (
|
||||
<div className="mt-2 text-xs text-red-600">
|
||||
<p>User: {errorData.user_info.username} ({errorData.user_info.role})</p>
|
||||
{errorData.user_info.is_superuser && (
|
||||
<p className="font-medium">⚠️ Superuser Account</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorData.help_text && (
|
||||
<div className="mt-3 p-2 bg-red-100 rounded text-xs text-red-700">
|
||||
<strong>Help:</strong> {errorData.help_text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="ml-3 flex-shrink-0 text-red-400 hover:text-red-600"
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Usage in account deletion component
|
||||
const AccountDeletionForm: React.FC = () => {
|
||||
const [error, setError] = useState<EnhancedApiError | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await api.accounts.requestAccountDeletion();
|
||||
toast.success('Account deletion request submitted successfully');
|
||||
} catch (err: any) {
|
||||
if (err.response?.data) {
|
||||
const apiError = err.response.data as EnhancedApiError;
|
||||
setError(apiError);
|
||||
|
||||
// Also show toast for immediate feedback
|
||||
toast.error(apiError.error.message);
|
||||
|
||||
// Log security-related errors for monitoring
|
||||
if (apiError.error.error_code === 'SUPERUSER_DELETION_BLOCKED') {
|
||||
console.warn('Superuser deletion attempt blocked:', {
|
||||
user: apiError.error.user_info?.username,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setError({
|
||||
status: "error",
|
||||
error: {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "An unexpected error occurred",
|
||||
error_code: "UNKNOWN_ERROR"
|
||||
},
|
||||
data: null
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto">
|
||||
<ErrorDisplay
|
||||
error={error}
|
||||
onDismiss={() => setError(null)}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleDeleteAccount}
|
||||
disabled={isLoading}
|
||||
className="w-full bg-red-600 text-white py-2 px-4 rounded hover:bg-red-700 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Processing...' : 'Delete Account'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Toast Notifications for Errors
|
||||
|
||||
For immediate user feedback, combine detailed error displays with toast notifications:
|
||||
|
||||
```typescript
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
const handleApiError = (error: EnhancedApiError) => {
|
||||
const { error: errorData } = error;
|
||||
|
||||
// Show immediate toast
|
||||
toast.error(errorData.message, {
|
||||
duration: 5000,
|
||||
position: 'top-right',
|
||||
});
|
||||
|
||||
// For critical errors, show additional context
|
||||
if (errorData.error_code === 'SUPERUSER_DELETION_BLOCKED') {
|
||||
toast.error('Superuser accounts cannot be deleted for security reasons', {
|
||||
duration: 8000,
|
||||
icon: '🔒',
|
||||
});
|
||||
}
|
||||
|
||||
if (errorData.error_code === 'ADMIN_DELETION_BLOCKED') {
|
||||
toast.error('Admin accounts with staff privileges cannot be deleted', {
|
||||
duration: 8000,
|
||||
icon: '⚠️',
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Error Boundary for Global Error Handling
|
||||
|
||||
Create an error boundary to catch and display API errors globally:
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: EnhancedApiError | null;
|
||||
}
|
||||
|
||||
class ApiErrorBoundary extends React.Component<
|
||||
React.PropsWithChildren<{}>,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: React.PropsWithChildren<{}>) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: any): ErrorBoundaryState {
|
||||
if (error.response?.data?.status === 'error') {
|
||||
return {
|
||||
hasError: true,
|
||||
error: error.response.data as EnhancedApiError
|
||||
};
|
||||
}
|
||||
return { hasError: true, error: null };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError && this.state.error) {
|
||||
return (
|
||||
<ErrorDisplay
|
||||
error={this.state.error}
|
||||
onDismiss={() => this.setState({ hasError: false, error: null })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
The system uses specific error codes for different scenarios:
|
||||
|
||||
- `NOT_AUTHENTICATED`: User not logged in
|
||||
- `PERMISSION_DENIED`: Insufficient permissions
|
||||
- `NOT_FOUND`: Resource not found
|
||||
- `VALIDATION_ERROR`: Invalid request data
|
||||
- `RATE_LIMITED`: Too many requests
|
||||
- `SUPERUSER_DELETION_BLOCKED`: Superuser account deletion attempt
|
||||
- `ADMIN_DELETION_BLOCKED`: Admin account with staff privileges deletion attempt
|
||||
- `ACCOUNT_DELETION_FAILED`: General account deletion failure
|
||||
- `SECURITY_VIOLATION`: Security policy violation detected
|
||||
|
||||
### Error Logging and Monitoring
|
||||
|
||||
For production applications, implement error logging:
|
||||
|
||||
```typescript
|
||||
const logError = (error: EnhancedApiError, context: string) => {
|
||||
// Log to your monitoring service (e.g., Sentry, LogRocket)
|
||||
console.error(`API Error in ${context}:`, {
|
||||
code: error.error.code,
|
||||
message: error.error.message,
|
||||
errorCode: error.error.error_code,
|
||||
userInfo: error.error.user_info,
|
||||
timestamp: new Date().toISOString(),
|
||||
context
|
||||
});
|
||||
|
||||
// Send to analytics if it's a security-related error
|
||||
if (error.error.error_code?.includes('DELETION_BLOCKED')) {
|
||||
// Track security events
|
||||
analytics.track('Security Event', {
|
||||
event: 'Account Deletion Blocked',
|
||||
errorCode: error.error.error_code,
|
||||
user: error.error.user_info?.username
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Pagination
|
||||
|
||||
|
||||
2864
docs/lib-api.ts
2864
docs/lib-api.ts
File diff suppressed because it is too large
Load Diff
2887
docs/types-api.ts
2887
docs/types-api.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user