Files
thrillwiki_django_no_react/docs/lib-api.ts
pacnpal bb7da85516 Refactor API structure and add comprehensive user management features
- Restructure API v1 with improved serializers organization
- Add user deletion requests and moderation queue system
- Implement bulk moderation operations and permissions
- Add user profile enhancements with display names and avatars
- Expand ride and park API endpoints with better filtering
- Add manufacturer API with detailed ride relationships
- Improve authentication flows and error handling
- Update frontend documentation and API specifications
2025-08-29 16:03:51 -04:00

730 lines
26 KiB
TypeScript

// ThrillWiki API Client for NextJS Frontend
// Last updated: 2025-08-29
// This file contains the complete API client implementation for ThrillWiki
import {
ApiResponse,
PaginatedResponse,
// Moderation types
ModerationReport,
CreateModerationReportData,
UpdateModerationReportData,
ModerationQueue,
CompleteQueueItemData,
ModerationAction,
CreateModerationActionData,
BulkOperation,
CreateBulkOperationData,
UserModerationProfile,
ModerationStatsData,
// Filter types
ModerationReportFilters,
ModerationQueueFilters,
ModerationActionFilters,
BulkOperationFilters,
ParkFilters,
RideFilters,
SearchFilters,
// Entity types
Park,
Ride,
Manufacturer,
RideModel,
ParkPhoto,
RidePhoto,
RideReview,
CreateRideReviewData,
// Auth types
LoginData,
SignupData,
AuthResponse,
UserProfile,
PasswordResetData,
PasswordChangeData,
// Stats types
GlobalStats,
TrendingContent,
// Utility types
ApiClientConfig,
RequestConfig,
} from '@/types/api';
// ============================================================================
// API Client Configuration
// ============================================================================
const DEFAULT_CONFIG: ApiClientConfig = {
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
timeout: 30000,
retries: 3,
retryDelay: 1000,
headers: {
'Content-Type': 'application/json',
},
};
// ============================================================================
// HTTP Client Class
// ============================================================================
class HttpClient {
private config: ApiClientConfig;
private authToken: string | null = null;
constructor(config: Partial<ApiClientConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
setAuthToken(token: string | null) {
this.authToken = token;
}
private getHeaders(customHeaders: Record<string, string> = {}): Record<string, string> {
const headers = { ...this.config.headers, ...customHeaders };
if (this.authToken) {
headers.Authorization = `Bearer ${this.authToken}`;
}
return headers;
}
private async makeRequest<T>(config: RequestConfig): Promise<ApiResponse<T>> {
const url = `${this.config.baseURL}${config.url}`;
const headers = this.getHeaders(config.headers);
const requestConfig: RequestInit = {
method: config.method,
headers,
signal: AbortSignal.timeout(config.timeout || this.config.timeout),
};
if (config.data && ['POST', 'PUT', 'PATCH'].includes(config.method)) {
requestConfig.body = JSON.stringify(config.data);
}
// Add query parameters for GET requests
const finalUrl = config.params && config.method === 'GET'
? `${url}?${new URLSearchParams(config.params).toString()}`
: url;
try {
const response = await fetch(finalUrl, requestConfig);
const data = await response.json();
if (!response.ok) {
return {
status: 'error',
data: null,
error: data.error || {
code: 'HTTP_ERROR',
message: `HTTP ${response.status}: ${response.statusText}`,
},
};
}
return data;
} catch (error) {
return {
status: 'error',
data: null,
error: {
code: 'NETWORK_ERROR',
message: error instanceof Error ? error.message : 'Network request failed',
},
};
}
}
async get<T>(url: string, params?: Record<string, any>, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.makeRequest<T>({ method: 'GET', url, params, headers });
}
async post<T>(url: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.makeRequest<T>({ method: 'POST', url, data, headers });
}
async put<T>(url: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.makeRequest<T>({ method: 'PUT', url, data, headers });
}
async patch<T>(url: string, data?: any, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.makeRequest<T>({ method: 'PATCH', url, data, headers });
}
async delete<T>(url: string, headers?: Record<string, string>): Promise<ApiResponse<T>> {
return this.makeRequest<T>({ method: 'DELETE', url, headers });
}
}
// ============================================================================
// API Client Class
// ============================================================================
export class ThrillWikiApiClient {
private http: HttpClient;
constructor(config?: Partial<ApiClientConfig>) {
this.http = new HttpClient(config);
}
setAuthToken(token: string | null) {
this.http.setAuthToken(token);
}
// ============================================================================
// Authentication API
// ============================================================================
auth = {
login: async (data: LoginData): Promise<ApiResponse<AuthResponse>> => {
return this.http.post<AuthResponse>('/auth/login/', data);
},
signup: async (data: SignupData): Promise<ApiResponse<AuthResponse>> => {
return this.http.post<AuthResponse>('/auth/signup/', data);
},
logout: async (): Promise<ApiResponse<null>> => {
return this.http.post<null>('/auth/logout/');
},
getCurrentUser: async (): Promise<ApiResponse<UserProfile>> => {
return this.http.get<UserProfile>('/auth/user/');
},
resetPassword: async (data: PasswordResetData): Promise<ApiResponse<null>> => {
return this.http.post<null>('/auth/password/reset/', data);
},
changePassword: async (data: PasswordChangeData): Promise<ApiResponse<null>> => {
return this.http.post<null>('/auth/password/change/', data);
},
getAuthStatus: async (): Promise<ApiResponse<{ is_authenticated: boolean; user?: UserProfile }>> => {
return this.http.get('/auth/status/');
},
getSocialProviders: async (): Promise<ApiResponse<any>> => {
return this.http.get('/auth/providers/');
},
};
// ============================================================================
// Moderation API
// ============================================================================
moderation = {
// Reports
reports: {
list: async (filters?: ModerationReportFilters): Promise<ApiResponse<PaginatedResponse<ModerationReport>>> => {
return this.http.get<PaginatedResponse<ModerationReport>>('/moderation/reports/', filters);
},
create: async (data: CreateModerationReportData): Promise<ApiResponse<ModerationReport>> => {
return this.http.post<ModerationReport>('/moderation/reports/', data);
},
get: async (id: number): Promise<ApiResponse<ModerationReport>> => {
return this.http.get<ModerationReport>(`/moderation/reports/${id}/`);
},
update: async (id: number, data: Partial<UpdateModerationReportData>): Promise<ApiResponse<ModerationReport>> => {
return this.http.patch<ModerationReport>(`/moderation/reports/${id}/`, data);
},
delete: async (id: number): Promise<ApiResponse<null>> => {
return this.http.delete<null>(`/moderation/reports/${id}/`);
},
assign: async (id: number, moderatorId: number): Promise<ApiResponse<ModerationReport>> => {
return this.http.post<ModerationReport>(`/moderation/reports/${id}/assign/`, { moderator_id: moderatorId });
},
resolve: async (id: number, resolutionAction: string, resolutionNotes?: string): Promise<ApiResponse<ModerationReport>> => {
return this.http.post<ModerationReport>(`/moderation/reports/${id}/resolve/`, {
resolution_action: resolutionAction,
resolution_notes: resolutionNotes || '',
});
},
getStats: async (): Promise<ApiResponse<ModerationStatsData>> => {
return this.http.get<ModerationStatsData>('/moderation/reports/stats/');
},
},
// Queue
queue: {
list: async (filters?: ModerationQueueFilters): Promise<ApiResponse<PaginatedResponse<ModerationQueue>>> => {
return this.http.get<PaginatedResponse<ModerationQueue>>('/moderation/queue/', filters);
},
create: async (data: Partial<ModerationQueue>): Promise<ApiResponse<ModerationQueue>> => {
return this.http.post<ModerationQueue>('/moderation/queue/', data);
},
get: async (id: number): Promise<ApiResponse<ModerationQueue>> => {
return this.http.get<ModerationQueue>(`/moderation/queue/${id}/`);
},
update: async (id: number, data: Partial<ModerationQueue>): Promise<ApiResponse<ModerationQueue>> => {
return this.http.patch<ModerationQueue>(`/moderation/queue/${id}/`, data);
},
delete: async (id: number): Promise<ApiResponse<null>> => {
return this.http.delete<null>(`/moderation/queue/${id}/`);
},
assign: async (id: number, moderatorId: number): Promise<ApiResponse<ModerationQueue>> => {
return this.http.post<ModerationQueue>(`/moderation/queue/${id}/assign/`, { moderator_id: moderatorId });
},
unassign: async (id: number): Promise<ApiResponse<ModerationQueue>> => {
return this.http.post<ModerationQueue>(`/moderation/queue/${id}/unassign/`);
},
complete: async (id: number, data: CompleteQueueItemData): Promise<ApiResponse<ModerationQueue>> => {
return this.http.post<ModerationQueue>(`/moderation/queue/${id}/complete/`, data);
},
getMyQueue: async (): Promise<ApiResponse<PaginatedResponse<ModerationQueue>>> => {
return this.http.get<PaginatedResponse<ModerationQueue>>('/moderation/queue/my_queue/');
},
},
// Actions
actions: {
list: async (filters?: ModerationActionFilters): Promise<ApiResponse<PaginatedResponse<ModerationAction>>> => {
return this.http.get<PaginatedResponse<ModerationAction>>('/moderation/actions/', filters);
},
create: async (data: CreateModerationActionData): Promise<ApiResponse<ModerationAction>> => {
return this.http.post<ModerationAction>('/moderation/actions/', data);
},
get: async (id: number): Promise<ApiResponse<ModerationAction>> => {
return this.http.get<ModerationAction>(`/moderation/actions/${id}/`);
},
update: async (id: number, data: Partial<ModerationAction>): Promise<ApiResponse<ModerationAction>> => {
return this.http.patch<ModerationAction>(`/moderation/actions/${id}/`, data);
},
delete: async (id: number): Promise<ApiResponse<null>> => {
return this.http.delete<null>(`/moderation/actions/${id}/`);
},
deactivate: async (id: number): Promise<ApiResponse<ModerationAction>> => {
return this.http.post<ModerationAction>(`/moderation/actions/${id}/deactivate/`);
},
getActive: async (): Promise<ApiResponse<PaginatedResponse<ModerationAction>>> => {
return this.http.get<PaginatedResponse<ModerationAction>>('/moderation/actions/active/');
},
getExpired: async (): Promise<ApiResponse<PaginatedResponse<ModerationAction>>> => {
return this.http.get<PaginatedResponse<ModerationAction>>('/moderation/actions/expired/');
},
},
// Bulk Operations
bulkOperations: {
list: async (filters?: BulkOperationFilters): Promise<ApiResponse<PaginatedResponse<BulkOperation>>> => {
return this.http.get<PaginatedResponse<BulkOperation>>('/moderation/bulk-operations/', filters);
},
create: async (data: CreateBulkOperationData): Promise<ApiResponse<BulkOperation>> => {
return this.http.post<BulkOperation>('/moderation/bulk-operations/', data);
},
get: async (id: string): Promise<ApiResponse<BulkOperation>> => {
return this.http.get<BulkOperation>(`/moderation/bulk-operations/${id}/`);
},
update: async (id: string, data: Partial<BulkOperation>): Promise<ApiResponse<BulkOperation>> => {
return this.http.patch<BulkOperation>(`/moderation/bulk-operations/${id}/`, data);
},
delete: async (id: string): Promise<ApiResponse<null>> => {
return this.http.delete<null>(`/moderation/bulk-operations/${id}/`);
},
cancel: async (id: string): Promise<ApiResponse<BulkOperation>> => {
return this.http.post<BulkOperation>(`/moderation/bulk-operations/${id}/cancel/`);
},
retry: async (id: string): Promise<ApiResponse<BulkOperation>> => {
return this.http.post<BulkOperation>(`/moderation/bulk-operations/${id}/retry/`);
},
getLogs: async (id: string): Promise<ApiResponse<{ logs: any[]; count: number }>> => {
return this.http.get(`/moderation/bulk-operations/${id}/logs/`);
},
getRunning: async (): Promise<ApiResponse<PaginatedResponse<BulkOperation>>> => {
return this.http.get<PaginatedResponse<BulkOperation>>('/moderation/bulk-operations/running/');
},
},
// User Moderation
users: {
get: async (id: number): Promise<ApiResponse<UserModerationProfile>> => {
return this.http.get<UserModerationProfile>(`/moderation/users/${id}/`);
},
moderate: async (id: number, data: CreateModerationActionData): Promise<ApiResponse<ModerationAction>> => {
return this.http.post<ModerationAction>(`/moderation/users/${id}/moderate/`, data);
},
search: async (params: { query?: string; role?: string; has_restrictions?: boolean }): Promise<ApiResponse<PaginatedResponse<any>>> => {
return this.http.get<PaginatedResponse<any>>('/moderation/users/search/', params);
},
getStats: async (): Promise<ApiResponse<{ total_actions: number; active_actions: number; expired_actions: number }>> => {
return this.http.get('/moderation/users/stats/');
},
},
};
// ============================================================================
// Parks API
// ============================================================================
parks = {
list: async (filters?: ParkFilters): Promise<ApiResponse<PaginatedResponse<Park>>> => {
return this.http.get<PaginatedResponse<Park>>('/parks/', filters);
},
get: async (slug: string): Promise<ApiResponse<Park>> => {
return this.http.get<Park>(`/parks/${slug}/`);
},
getRides: async (parkSlug: string, filters?: RideFilters): Promise<ApiResponse<PaginatedResponse<Ride>>> => {
return this.http.get<PaginatedResponse<Ride>>(`/parks/${parkSlug}/rides/`, filters);
},
getPhotos: async (parkSlug: string, filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<ParkPhoto>>> => {
return this.http.get<PaginatedResponse<ParkPhoto>>(`/parks/${parkSlug}/photos/`, filters);
},
// Park operators and owners
operators: {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<any>>> => {
return this.http.get<PaginatedResponse<any>>('/parks/operators/', filters);
},
get: async (slug: string): Promise<ApiResponse<any>> => {
return this.http.get(`/parks/operators/${slug}/`);
},
},
owners: {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<any>>> => {
return this.http.get<PaginatedResponse<any>>('/parks/owners/', filters);
},
get: async (slug: string): Promise<ApiResponse<any>> => {
return this.http.get(`/parks/owners/${slug}/`);
},
},
};
// ============================================================================
// Rides API
// ============================================================================
rides = {
list: async (filters?: RideFilters): Promise<ApiResponse<PaginatedResponse<Ride>>> => {
return this.http.get<PaginatedResponse<Ride>>('/rides/', filters);
},
get: async (parkSlug: string, rideSlug: string): Promise<ApiResponse<Ride>> => {
return this.http.get<Ride>(`/rides/${parkSlug}/${rideSlug}/`);
},
getPhotos: async (parkSlug: string, rideSlug: string, filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<RidePhoto>>> => {
return this.http.get<PaginatedResponse<RidePhoto>>(`/rides/${parkSlug}/${rideSlug}/photos/`, filters);
},
getReviews: async (parkSlug: string, rideSlug: string, filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<RideReview>>> => {
return this.http.get<PaginatedResponse<RideReview>>(`/rides/${parkSlug}/${rideSlug}/reviews/`, filters);
},
createReview: async (parkSlug: string, rideSlug: string, data: CreateRideReviewData): Promise<ApiResponse<RideReview>> => {
return this.http.post<RideReview>(`/rides/${parkSlug}/${rideSlug}/reviews/`, data);
},
// Manufacturers
manufacturers: {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<Manufacturer>>> => {
return this.http.get<PaginatedResponse<Manufacturer>>('/rides/manufacturers/', filters);
},
get: async (slug: string): Promise<ApiResponse<Manufacturer>> => {
return this.http.get<Manufacturer>(`/rides/manufacturers/${slug}/`);
},
getRides: async (slug: string, filters?: RideFilters): Promise<ApiResponse<PaginatedResponse<Ride>>> => {
return this.http.get<PaginatedResponse<Ride>>(`/rides/manufacturers/${slug}/rides/`, filters);
},
getModels: async (slug: string, filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<RideModel>>> => {
return this.http.get<PaginatedResponse<RideModel>>(`/rides/manufacturers/${slug}/models/`, filters);
},
},
// Designers
designers: {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<any>>> => {
return this.http.get<PaginatedResponse<any>>('/rides/designers/', filters);
},
get: async (slug: string): Promise<ApiResponse<any>> => {
return this.http.get(`/rides/designers/${slug}/`);
},
},
// Ride Models
models: {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<RideModel>>> => {
return this.http.get<PaginatedResponse<RideModel>>('/rides/models/', filters);
},
get: async (manufacturerSlug: string, modelSlug: string): Promise<ApiResponse<RideModel>> => {
return this.http.get<RideModel>(`/rides/models/${manufacturerSlug}/${modelSlug}/`);
},
},
};
// ============================================================================
// Statistics API
// ============================================================================
stats = {
getGlobal: async (): Promise<ApiResponse<GlobalStats>> => {
return this.http.get<GlobalStats>('/stats/');
},
recalculate: async (): Promise<ApiResponse<null>> => {
return this.http.post<null>('/stats/recalculate/');
},
getTrending: async (params?: { content_type?: string; time_period?: string }): Promise<ApiResponse<TrendingContent>> => {
return this.http.get<TrendingContent>('/trending/', params);
},
getNewContent: async (): Promise<ApiResponse<any>> => {
return this.http.get('/new-content/');
},
triggerTrendingCalculation: async (): Promise<ApiResponse<null>> => {
return this.http.post<null>('/trending/calculate/');
},
getLatestReviews: async (params?: { limit?: number; park?: string; ride?: string }): Promise<ApiResponse<PaginatedResponse<RideReview>>> => {
return this.http.get<PaginatedResponse<RideReview>>('/reviews/latest/', params);
},
};
// ============================================================================
// Rankings API
// ============================================================================
rankings = {
list: async (filters?: SearchFilters): Promise<ApiResponse<PaginatedResponse<any>>> => {
return this.http.get<PaginatedResponse<any>>('/rankings/', filters);
},
calculate: async (): Promise<ApiResponse<null>> => {
return this.http.post<null>('/rankings/calculate/');
},
};
// ============================================================================
// Health Check API
// ============================================================================
health = {
check: async (): Promise<ApiResponse<any>> => {
return this.http.get('/health/');
},
simple: async (): Promise<ApiResponse<{ status: string }>> => {
return this.http.get('/health/simple/');
},
performance: async (): Promise<ApiResponse<any>> => {
return this.http.get('/health/performance/');
},
};
// ============================================================================
// Accounts API
// ============================================================================
accounts = {
getProfile: async (username: string): Promise<ApiResponse<UserProfile>> => {
return this.http.get<UserProfile>(`/accounts/users/${username}/`);
},
updateProfile: async (data: Partial<UserProfile>): Promise<ApiResponse<UserProfile>> => {
return this.http.patch<UserProfile>('/accounts/profile/', data);
},
getSettings: async (): Promise<ApiResponse<any>> => {
return this.http.get('/accounts/settings/');
},
updateSettings: async (data: any): Promise<ApiResponse<any>> => {
return this.http.patch('/accounts/settings/', data);
},
};
// ============================================================================
// Maps API
// ============================================================================
maps = {
getParkLocations: async (params?: { bounds?: string; zoom?: number }): Promise<ApiResponse<any>> => {
return this.http.get('/maps/park-locations/', params);
},
getRideLocations: async (parkSlug: string, params?: { bounds?: string; zoom?: number }): Promise<ApiResponse<any>> => {
return this.http.get(`/maps/parks/${parkSlug}/ride-locations/`, params);
},
getUnifiedMap: async (params?: { bounds?: string; zoom?: number; include_parks?: boolean; include_rides?: boolean }): Promise<ApiResponse<any>> => {
return this.http.get('/maps/unified/', params);
},
};
// ============================================================================
// Email API
// ============================================================================
email = {
sendTestEmail: async (data: { to: string; subject: string; message: string }): Promise<ApiResponse<null>> => {
return this.http.post<null>('/email/send-test/', data);
},
getTemplates: async (): Promise<ApiResponse<any[]>> => {
return this.http.get('/email/templates/');
},
};
}
// ============================================================================
// Default Export and Utilities
// ============================================================================
// Create default client instance
export const apiClient = new ThrillWikiApiClient();
// Utility functions for common operations
export const apiUtils = {
// Set authentication token for all requests
setAuthToken: (token: string | null) => {
apiClient.setAuthToken(token);
},
// Check if response is successful
isSuccess: <T>(response: ApiResponse<T>): response is ApiResponse<T> & { status: 'success'; data: T } => {
return response.status === 'success' && response.data !== null;
},
// Check if response is an error
isError: <T>(response: ApiResponse<T>): response is ApiResponse<T> & { status: 'error'; error: NonNullable<ApiResponse<T>['error']> } => {
return response.status === 'error' && response.error !== null;
},
// Extract data from successful response or throw error
unwrap: <T>(response: ApiResponse<T>): T => {
if (apiUtils.isSuccess(response)) {
return response.data;
}
throw new Error(response.error?.message || 'API request failed');
},
// Handle paginated responses
extractPaginatedData: <T>(response: ApiResponse<PaginatedResponse<T>>): T[] => {
if (apiUtils.isSuccess(response)) {
return response.data.results;
}
return [];
},
// Build query string from filters
buildQueryString: (filters: Record<string, any>): string => {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
if (Array.isArray(value)) {
value.forEach(v => params.append(key, String(v)));
} else {
params.append(key, String(value));
}
}
});
return params.toString();
},
// Format error message for display
formatError: (error: ApiResponse<any>['error']): string => {
if (!error) return 'Unknown error occurred';
if (error.details && typeof error.details === 'object') {
// Handle validation errors
const fieldErrors = Object.entries(error.details)
.map(([field, messages]) => `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`)
.join('; ');
return fieldErrors || error.message;
}
return error.message;
},
// Check if user has required role
hasRole: (user: UserProfile | null, requiredRole: UserProfile['role']): boolean => {
if (!user) return false;
const roleHierarchy = ['USER', 'MODERATOR', 'ADMIN', 'SUPERUSER'];
const userRoleIndex = roleHierarchy.indexOf(user.role);
const requiredRoleIndex = roleHierarchy.indexOf(requiredRole);
return userRoleIndex >= requiredRoleIndex;
},
// Check if user can moderate
canModerate: (user: UserProfile | null): boolean => {
return apiUtils.hasRole(user, 'MODERATOR');
},
// Check if user is admin
isAdmin: (user: UserProfile | null): boolean => {
return apiUtils.hasRole(user, 'ADMIN');
},
};
// Export types for convenience
export type {
ApiResponse,
PaginatedResponse,
ModerationReport,
ModerationQueue,
ModerationAction,
BulkOperation,
UserModerationProfile,
Park,
Ride,
Manufacturer,
RideModel,
UserProfile,
GlobalStats,
TrendingContent,
} from './types-api';
export default apiClient;