diff --git a/DEPLOYMENT_FIXES.md b/DEPLOYMENT_FIXES.md index 49104dd..64081bb 100644 --- a/DEPLOYMENT_FIXES.md +++ b/DEPLOYMENT_FIXES.md @@ -1,64 +1,84 @@ -# 🔧 Deployment Issue Fixes - -## 🔍 Issues Identified from Latest Log - -### **Issue 1: TypeScript JSX Configuration Missing** ❌→✅ -**Problem**: TypeScript compilation failing with `error TS6142: '--jsx' is not set` -**Root Cause**: Generated tsconfig.json in Docker was missing JSX configuration -**Fix Applied**: Added `"jsx":"react-jsx"` to the tsconfig.json generation in Dockerfile -**Line Fixed**: Line 77 in Dockerfile - -### **Issue 2: Health Check Tool Mismatch** ❌→✅ -**Problem**: Health checks failing with `wget: can't connect to remote host: Connection refused` -**Root Cause**: -- Dockerfile uses `curl` for health checks -- Coolify deployment system uses `wget` for health checks -- Tool mismatch causing health check failures - -**Fix Applied**: -1. Added `wget` installation alongside `curl` -2. Updated health check command to support both tools: `curl -f http://localhost/ || wget -q --spider http://localhost/ || exit 1` - -### **Issue 3: Container Health Check Endpoint** ❌→✅ -**Problem**: Health check trying to access `/health` endpoint that doesn't exist -**Fix Applied**: Changed health check to use root path `/` which always exists for Nginx - -## 📋 Changes Made - -### **1. Updated Dockerfile (Lines 77, 89, 113)** -```dockerfile -# Fixed TypeScript JSX configuration -RUN echo '{"compilerOptions":{..."jsx":"react-jsx"...}}' > tsconfig.json - -# Added wget for Coolify compatibility -RUN apk add --no-cache curl wget - -# Fixed health check with fallback -CMD curl -f http://localhost/ || wget -q --spider http://localhost/ || exit 1 -``` - -## ✅ Expected Results - -After these fixes: - -1. **TypeScript Build**: ✅ Should compile `.tsx` files successfully -2. **Health Check**: ✅ Should pass using either curl or wget -3. **Container Status**: ✅ Should show as healthy -4. **Deployment**: ✅ Should complete without rollback - -## 🎯 Root Cause Analysis - -The deployment failures were caused by: -1. **Build Configuration**: Missing JSX support in generated TypeScript config -2. **Health Check Compatibility**: Tool mismatch between Docker image and deployment platform -3. **Endpoint Mismatch**: Health check looking for non-existent endpoint - -## 🚀 Next Deployment - -The next deployment should: -- ✅ Build successfully with JSX support -- ✅ Pass health checks with both curl and wget -- ✅ Complete without rollbacks -- ✅ Result in a fully functional application - -**Status**: Ready for redeployment with fixes applied! 🎉 +# 🔧 Deployment Issue Fixes + +## 🔍 Issues Identified from Latest Log + +### **Issue 1: TypeScript JSX Configuration Missing** ❌→✅ +**Problem**: TypeScript compilation failing with `error TS6142: '--jsx' is not set` +**Root Cause**: Generated tsconfig.json in Docker was missing JSX configuration +**Fix Applied**: Added `"jsx":"react-jsx"` to the tsconfig.json generation in Dockerfile +**Line Fixed**: Line 77 in Dockerfile + +### **Issue 2: Health Check Tool Mismatch** ❌→✅ +**Problem**: Health checks failing with `wget: can't connect to remote host: Connection refused` +**Root Cause**: +- Dockerfile uses `curl` for health checks +- Coolify deployment system uses `wget` for health checks +- Tool mismatch causing health check failures + +**Fix Applied**: +1. Added `wget` installation alongside `curl` +2. Updated health check command to support both tools: `curl -f http://localhost/ || wget -q --spider http://localhost/ || exit 1` + +### **Issue 3: Container Health Check Endpoint** ❌→✅ +**Problem**: Health check trying to access `/health` endpoint that doesn't exist +**Fix Applied**: Changed health check to use root path `/` which always exists for Nginx + +### **Issue 4: Mock API Build Order & TypeScript Errors** ❌→✅ +**Problem**: +- TypeScript compilation errors in mock API functions +- Build order issue: source files copied before mock API creation +- Server.ts file causing compilation errors + +**Root Cause**: +- Mock API files created after source code copy +- TypeScript compilation happening before mock API is ready +- Server.ts file not needed for frontend demo + +**Fix Applied**: +1. Reordered Dockerfile: create mock API structure BEFORE copying source code +2. Added `rm -f src/server.ts` to remove server file +3. Fixed function signatures in mock API (added missing parameters) +4. Fixed useCreateScript hook to properly handle userId parameter +5. Fixed server.ts createScript call to pass userId parameter + +## 📋 Changes Made + +### **1. Updated Dockerfile (Lines 77, 89, 113)** +```dockerfile +# Fixed TypeScript JSX configuration +RUN echo '{"compilerOptions":{..."jsx":"react-jsx"...}}' > tsconfig.json + +# Added wget for Coolify compatibility +RUN apk add --no-cache curl wget + +# Fixed health check with fallback +CMD curl -f http://localhost/ || wget -q --spider http://localhost/ || exit 1 +``` + +## ✅ Expected Results + +After these fixes: + +1. **TypeScript Build**: ✅ Should compile `.tsx` files successfully +2. **Mock API**: ✅ Should compile without TypeScript errors +3. **Health Check**: ✅ Should pass using either curl or wget +4. **Container Status**: ✅ Should show as healthy +5. **Deployment**: ✅ Should complete without rollback +6. **Frontend**: ✅ Should load and function properly with mock API + +## 🎯 Root Cause Analysis + +The deployment failures were caused by: +1. **Build Configuration**: Missing JSX support in generated TypeScript config +2. **Health Check Compatibility**: Tool mismatch between Docker image and deployment platform +3. **Endpoint Mismatch**: Health check looking for non-existent endpoint + +## 🚀 Next Deployment + +The next deployment should: +- ✅ Build successfully with JSX support +- ✅ Pass health checks with both curl and wget +- ✅ Complete without rollbacks +- ✅ Result in a fully functional application + +**Status**: Ready for redeployment with fixes applied! 🎉 diff --git a/Dockerfile b/Dockerfile index 894eadb..4c3021a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,6 @@ COPY package*.json ./ # Install dependencies with proper npm cache handling RUN npm ci --only=production=false --silent -# Copy source code -COPY . . - # Set build-time environment variables ARG VITE_APP_NAME="ScriptShare" ARG VITE_APP_URL="https://scriptshare.example.com" @@ -39,10 +36,14 @@ RUN npm install # Remove problematic server-side API files for frontend-only build RUN rm -rf src/lib/api || true RUN rm -rf src/lib/db || true +RUN rm -f src/server.ts || true # Create mock API layer for frontend demo RUN mkdir -p src/lib/api src/lib/db +# Copy source code AFTER creating mock API structure +COPY . . + # Create mock database files RUN echo "export const db = {};" > src/lib/db/index.ts RUN echo "export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};" > src/lib/db/schema.ts @@ -56,7 +57,7 @@ RUN printf 'import { nanoid } from "nanoid";\nexport const generateId = () => na RUN printf 'export interface LoginCredentials {\n email: string;\n password: string;\n}\nexport interface RegisterData {\n email: string;\n username: string;\n displayName: string;\n password: string;\n}\nexport interface AuthToken {\n token: string;\n user: any;\n}\nexport async function login(credentials: LoginCredentials): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function register(data: RegisterData): Promise {\n return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } };\n}\nexport async function refreshToken(token: string): Promise {\n return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };\n}\nexport async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise {\n return true;\n}' > src/lib/api/auth.ts # Mock scripts API with individual function exports -RUN printf 'export interface ScriptFilters {\n search?: string;\n categories?: string[];\n compatibleOs?: string[];\n sortBy?: string;\n limit?: number;\n isApproved?: boolean;\n}\nexport interface UpdateScriptData {\n name?: string;\n description?: string;\n content?: string;\n}\nexport interface CreateScriptData {\n name: string;\n description: string;\n content: string;\n categories: string[];\n compatibleOs: string[];\n tags?: string[];\n}\nexport async function getScripts(filters?: ScriptFilters) {\n return { scripts: [], total: 0 };\n}\nexport async function getScriptById(id: string) {\n return null;\n}\nexport async function getPopularScripts() {\n return [];\n}\nexport async function getRecentScripts() {\n return [];\n}\nexport async function createScript(data: CreateScriptData, userId: string) {\n return { id: "mock-script-id", ...data, authorId: userId };\n}\nexport async function updateScript(id: string, data: UpdateScriptData, userId: string) {\n return { id, ...data };\n}\nexport async function deleteScript(id: string, userId: string) {\n return { success: true };\n}\nexport async function moderateScript(id: string, isApproved: boolean, moderatorId: string) {\n return { id, isApproved };\n}\nexport async function incrementViewCount(id: string) {\n return { success: true };\n}\nexport async function incrementDownloadCount(id: string) {\n return { success: true };\n}' > src/lib/api/scripts.ts +RUN printf 'export interface ScriptFilters {\n search?: string;\n categories?: string[];\n compatibleOs?: string[];\n sortBy?: string;\n limit?: number;\n isApproved?: boolean;\n}\nexport interface UpdateScriptData {\n name?: string;\n description?: string;\n content?: string;\n}\nexport interface CreateScriptData {\n name: string;\n description: string;\n content: string;\n categories: string[];\n compatibleOs: string[];\n tags?: string[];\n}\nexport async function getScripts(filters?: ScriptFilters) {\n return { scripts: [], total: 0 };\n}\nexport async function getScriptById(id: string) {\n return null;\n}\nexport async function getPopularScripts(limit?: number) {\n return [];\n}\nexport async function getRecentScripts(limit?: number) {\n return [];\n}\nexport async function createScript(data: CreateScriptData, userId: string) {\n return { id: "mock-script-id", ...data, authorId: userId };\n}\nexport async function updateScript(id: string, data: UpdateScriptData, userId: string) {\n return { id, ...data };\n}\nexport async function deleteScript(id: string, userId: string) {\n return { success: true };\n}\nexport async function moderateScript(id: string, isApproved: boolean, moderatorId: string) {\n return { id, isApproved };\n}\nexport async function incrementViewCount(id: string) {\n return { success: true };\n}\nexport async function incrementDownloadCount(id: string) {\n return { success: true };\n}' > src/lib/api/scripts.ts # Mock ratings API with individual function exports RUN printf 'export interface CreateRatingData {\n scriptId: string;\n userId: string;\n rating: number;\n}\nexport async function rateScript(data: CreateRatingData) {\n return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserRating(scriptId: string, userId: string) {\n return null;\n}\nexport async function getScriptRatings(scriptId: string) {\n return [];\n}\nexport async function getScriptRatingStats(scriptId: string) {\n return { averageRating: 0, totalRatings: 0, distribution: [] };\n}\nexport async function deleteRating(scriptId: string, userId: string) {\n return { success: true };\n}' > src/lib/api/ratings.ts @@ -65,7 +66,7 @@ RUN printf 'export interface CreateRatingData {\n scriptId: string;\n userId: RUN printf 'export interface TrackEventData {\n scriptId: string;\n eventType: string;\n userId?: string;\n userAgent?: string;\n ipAddress?: string;\n referrer?: string;\n}\nexport interface AnalyticsFilters {\n scriptId?: string;\n eventType?: string;\n startDate?: Date;\n endDate?: Date;\n userId?: string;\n}\nexport async function trackEvent(data: TrackEventData) {\n return { success: true };\n}\nexport async function getAnalyticsEvents(filters?: AnalyticsFilters) {\n return [];\n}\nexport async function getScriptAnalytics(scriptId: string, days?: number) {\n return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 };\n}\nexport async function getPlatformAnalytics(days?: number) {\n return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 };\n}\nexport async function getUserAnalytics(userId: string, days?: number) {\n return { userScripts: [], recentActivity: [], periodDays: days || 30 };\n}' > src/lib/api/analytics.ts # Mock collections API with individual function exports -RUN printf 'export interface CreateCollectionData {\n name: string;\n description?: string;\n authorId: string;\n isPublic?: boolean;\n}\nexport interface UpdateCollectionData {\n name?: string;\n description?: string;\n isPublic?: boolean;\n}\nexport async function createCollection(data: CreateCollectionData) {\n return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getCollectionById(id: string) {\n return null;\n}\nexport async function getUserCollections(userId: string) {\n return [];\n}\nexport async function getPublicCollections(limit?: number, offset?: number) {\n return [];\n}\nexport async function updateCollection(id: string, data: UpdateCollectionData, userId: string) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function deleteCollection(id: string, userId: string) {\n return { success: true };\n}\nexport async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) {\n return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() };\n}\nexport async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) {\n return { success: true };\n}\nexport async function isScriptInCollection(collectionId: string, scriptId: string) {\n return false;\n}' > src/lib/api/collections.ts +RUN printf 'export interface CreateCollectionData {\n name: string;\n description?: string;\n authorId: string;\n isPublic?: boolean;\n}\nexport interface UpdateCollectionData {\n name?: string;\n description?: string;\n isPublic?: boolean;\n}\nexport async function createCollection(data: CreateCollectionData) {\n return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getCollectionById(id: string) {\n return null;\n}\nexport async function getUserCollections(userId: string) {\n return [];\n}\nexport async function getPublicCollections(limit?: number, offset?: number) {\n return [];\n}\nexport async function updateCollection(id: string, data: UpdateCollectionData, userId: string) {\n return { id, ...data, authorId: userId, updatedAt: new Date() };\n}\nexport async function deleteCollection(id: string, userId: string) {\n return { success: true };\n}\nexport async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) {\n return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() };\n}\nexport async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) {\n return { success: true };\n}\nexport async function isScriptInCollection(collectionId: string, scriptId: string) {\n return false;\n}' > src/lib/api/collections.ts # Mock users API with individual function exports RUN printf 'export interface CreateUserData {\n email: string;\n username: string;\n displayName: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport interface UpdateUserData {\n username?: string;\n displayName?: string;\n avatarUrl?: string;\n bio?: string;\n}\nexport async function createUser(data: CreateUserData) {\n return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() };\n}\nexport async function getUserById(id: string) {\n return null;\n}\nexport async function getUserByEmail(email: string) {\n return null;\n}\nexport async function getUserByUsername(username: string) {\n return null;\n}\nexport async function updateUser(id: string, data: UpdateUserData) {\n return { id, ...data, updatedAt: new Date() };\n}\nexport async function updateUserPermissions(id: string, permissions: any) {\n return { id, ...permissions, updatedAt: new Date() };\n}\nexport async function searchUsers(query: string, limit?: number) {\n return [];\n}\nexport async function getAllUsers(limit?: number, offset?: number) {\n return [];\n}' > src/lib/api/users.ts diff --git a/src/hooks/useScripts.ts b/src/hooks/useScripts.ts index 5d8511f..b8cd0a3 100644 --- a/src/hooks/useScripts.ts +++ b/src/hooks/useScripts.ts @@ -1,139 +1,140 @@ -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import * as scriptsApi from '@/lib/api/scripts'; -import { showSuccess, showError } from '@/utils/toast'; - -// Query keys -export const scriptKeys = { - all: ['scripts'] as const, - lists: () => [...scriptKeys.all, 'list'] as const, - list: (filters: scriptsApi.ScriptFilters) => [...scriptKeys.lists(), filters] as const, - details: () => [...scriptKeys.all, 'detail'] as const, - detail: (id: string) => [...scriptKeys.details(), id] as const, - popular: () => [...scriptKeys.all, 'popular'] as const, - recent: () => [...scriptKeys.all, 'recent'] as const, -}; - -// Get scripts with filters -export function useScripts(filters: scriptsApi.ScriptFilters = {}) { - return useQuery({ - queryKey: scriptKeys.list(filters), - queryFn: () => scriptsApi.getScripts(filters), - staleTime: 5 * 60 * 1000, // 5 minutes - }); -} - -// Get script by ID -export function useScript(id: string) { - return useQuery({ - queryKey: scriptKeys.detail(id), - queryFn: () => scriptsApi.getScriptById(id), - enabled: !!id, - staleTime: 5 * 60 * 1000, - }); -} - -// Get popular scripts -export function usePopularScripts(limit?: number) { - return useQuery({ - queryKey: [...scriptKeys.popular(), limit], - queryFn: () => scriptsApi.getPopularScripts(limit), - staleTime: 10 * 60 * 1000, // 10 minutes - }); -} - -// Get recent scripts -export function useRecentScripts(limit?: number) { - return useQuery({ - queryKey: [...scriptKeys.recent(), limit], - queryFn: () => scriptsApi.getRecentScripts(limit), - staleTime: 5 * 60 * 1000, - }); -} - -// Create script mutation -export function useCreateScript() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: scriptsApi.createScript, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); - showSuccess('Script created successfully!'); - }, - onError: (error: any) => { - showError(error.message || 'Failed to create script'); - }, - }); -} - -// Update script mutation -export function useUpdateScript() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ id, data, userId }: { id: string; data: scriptsApi.UpdateScriptData; userId: string }) => - scriptsApi.updateScript(id, data, userId), - onSuccess: (data: any) => { - queryClient.invalidateQueries({ queryKey: scriptKeys.detail(data.id) }); - queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); - showSuccess('Script updated successfully!'); - }, - onError: (error: any) => { - showError(error.message || 'Failed to update script'); - }, - }); -} - -// Delete script mutation -export function useDeleteScript() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ id, userId }: { id: string; userId: string }) => - scriptsApi.deleteScript(id, userId), - onSuccess: (_, variables) => { - queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); - queryClient.removeQueries({ queryKey: scriptKeys.detail(variables.id) }); - showSuccess('Script deleted successfully!'); - }, - onError: (error: any) => { - showError(error.message || 'Failed to delete script'); - }, - }); -} - -// Moderate script mutation (admin only) -export function useModerateScript() { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: ({ id, isApproved, moderatorId }: { id: string; isApproved: boolean; moderatorId: string }) => - scriptsApi.moderateScript(id, isApproved, moderatorId), - onSuccess: (data: any) => { - queryClient.invalidateQueries({ queryKey: scriptKeys.detail(data.id) }); - queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); - showSuccess(`Script ${data.isApproved ? 'approved' : 'rejected'} successfully!`); - }, - onError: (error: any) => { - showError(error.message || 'Failed to moderate script'); - }, - }); -} - -// Track view mutation -export function useTrackView() { - return useMutation({ - mutationFn: scriptsApi.incrementViewCount, - // Don't show success/error messages for view tracking - }); -} - -// Track download mutation -export function useTrackDownload() { - return useMutation({ - mutationFn: scriptsApi.incrementDownloadCount, - onSuccess: () => { - showSuccess('Download started!'); - }, - }); -} +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import * as scriptsApi from '@/lib/api/scripts'; +import { showSuccess, showError } from '@/utils/toast'; + +// Query keys +export const scriptKeys = { + all: ['scripts'] as const, + lists: () => [...scriptKeys.all, 'list'] as const, + list: (filters: scriptsApi.ScriptFilters) => [...scriptKeys.lists(), filters] as const, + details: () => [...scriptKeys.all, 'detail'] as const, + detail: (id: string) => [...scriptKeys.details(), id] as const, + popular: () => [...scriptKeys.all, 'popular'] as const, + recent: () => [...scriptKeys.all, 'recent'] as const, +}; + +// Get scripts with filters +export function useScripts(filters: scriptsApi.ScriptFilters = {}) { + return useQuery({ + queryKey: scriptKeys.list(filters), + queryFn: () => scriptsApi.getScripts(filters), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +// Get script by ID +export function useScript(id: string) { + return useQuery({ + queryKey: scriptKeys.detail(id), + queryFn: () => scriptsApi.getScriptById(id), + enabled: !!id, + staleTime: 5 * 60 * 1000, + }); +} + +// Get popular scripts +export function usePopularScripts(limit?: number) { + return useQuery({ + queryKey: [...scriptKeys.popular(), limit], + queryFn: () => scriptsApi.getPopularScripts(limit), + staleTime: 10 * 60 * 1000, // 10 minutes + }); +} + +// Get recent scripts +export function useRecentScripts(limit?: number) { + return useQuery({ + queryKey: [...scriptKeys.recent(), limit], + queryFn: () => scriptsApi.getRecentScripts(limit), + staleTime: 5 * 60 * 1000, + }); +} + +// Create script mutation +export function useCreateScript() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ data, userId }: { data: scriptsApi.CreateScriptData; userId: string }) => + scriptsApi.createScript(data, userId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); + showSuccess('Script created successfully!'); + }, + onError: (error: any) => { + showError(error.message || 'Failed to create script'); + }, + }); +} + +// Update script mutation +export function useUpdateScript() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, data, userId }: { id: string; data: scriptsApi.UpdateScriptData; userId: string }) => + scriptsApi.updateScript(id, data, userId), + onSuccess: (data: any) => { + queryClient.invalidateQueries({ queryKey: scriptKeys.detail(data.id) }); + queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); + showSuccess('Script updated successfully!'); + }, + onError: (error: any) => { + showError(error.message || 'Failed to update script'); + }, + }); +} + +// Delete script mutation +export function useDeleteScript() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, userId }: { id: string; userId: string }) => + scriptsApi.deleteScript(id, userId), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); + queryClient.removeQueries({ queryKey: scriptKeys.detail(variables.id) }); + showSuccess('Script deleted successfully!'); + }, + onError: (error: any) => { + showError(error.message || 'Failed to delete script'); + }, + }); +} + +// Moderate script mutation (admin only) +export function useModerateScript() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, isApproved, moderatorId }: { id: string; isApproved: boolean; moderatorId: string }) => + scriptsApi.moderateScript(id, isApproved, moderatorId), + onSuccess: (data: any) => { + queryClient.invalidateQueries({ queryKey: scriptKeys.detail(data.id) }); + queryClient.invalidateQueries({ queryKey: scriptKeys.lists() }); + showSuccess(`Script ${data.isApproved ? 'approved' : 'rejected'} successfully!`); + }, + onError: (error: any) => { + showError(error.message || 'Failed to moderate script'); + }, + }); +} + +// Track view mutation +export function useTrackView() { + return useMutation({ + mutationFn: scriptsApi.incrementViewCount, + // Don't show success/error messages for view tracking + }); +} + +// Track download mutation +export function useTrackDownload() { + return useMutation({ + mutationFn: scriptsApi.incrementDownloadCount, + onSuccess: () => { + showSuccess('Download started!'); + }, + }); +} diff --git a/src/server.ts b/src/server.ts index 943b91c..c021afb 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,179 +1,179 @@ -import express, { Request, Response, NextFunction } from 'express'; -import cors from 'cors'; -import { getAllUsers, getUserById } from './lib/api/users.js'; -import { getScripts, getScriptById, createScript } from './lib/api/scripts.js'; -import { login, register } from './lib/api/auth.js'; -import { rateScript, getScriptRatingStats } from './lib/api/ratings.js'; -import { getPlatformAnalytics, trackEvent } from './lib/api/analytics.js'; -import { getUserCollections, getPublicCollections } from './lib/api/collections.js'; - -const app = express(); -const PORT = process.env.PORT || 3000; - -// Middleware -app.use(cors({ - origin: process.env.CORS_ORIGIN || '*', - credentials: true -})); -app.use(express.json({ limit: '10mb' })); -app.use(express.urlencoded({ extended: true })); - -// Health check endpoint -app.get('/api/health', (_req: Request, res: Response) => { - res.json({ status: 'ok', timestamp: new Date().toISOString() }); -}); - -// Auth routes -app.post('/api/auth/login', async (req: Request, res: Response) => { - try { - const result = await login(req.body); - res.json(result); - } catch (error) { - console.error('Login error:', error); - res.status(401).json({ error: 'Invalid credentials' }); - } -}); - -app.post('/api/auth/register', async (req: Request, res: Response) => { - try { - const result = await register(req.body); - res.json(result); - } catch (error) { - console.error('Register error:', error); - res.status(400).json({ error: 'Registration failed' }); - } -}); - -// Scripts routes -app.get('/api/scripts', async (req: Request, res: Response) => { - try { - const result = await getScripts(req.query); - res.json(result); - } catch (error) { - console.error('Get scripts error:', error); - res.status(500).json({ error: 'Failed to fetch scripts' }); - } -}); - -app.get('/api/scripts/:id', async (req: Request, res: Response) => { - try { - const script = await getScriptById(req.params.id); - if (!script) { - return res.status(404).json({ error: 'Script not found' }); - } - res.json(script); - } catch (error) { - console.error('Get script error:', error); - res.status(500).json({ error: 'Failed to fetch script' }); - } -}); - -app.post('/api/scripts', async (req: Request, res: Response) => { - try { - const userId = req.headers['x-user-id'] as string; - if (!userId) { - return res.status(401).json({ error: 'Unauthorized' }); - } - const result = await createScript(req.body); - res.json(result); - } catch (error) { - console.error('Create script error:', error); - res.status(500).json({ error: 'Failed to create script' }); - } -}); - -// Users routes -app.get('/api/users', async (_req: Request, res: Response) => { - try { - const result = await getAllUsers(); - res.json(result); - } catch (error) { - console.error('Get users error:', error); - res.status(500).json({ error: 'Failed to fetch users' }); - } -}); - -app.get('/api/users/:id', async (req: Request, res: Response) => { - try { - const user = await getUserById(req.params.id); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } - res.json(user); - } catch (error) { - console.error('Get user error:', error); - res.status(500).json({ error: 'Failed to fetch user' }); - } -}); - -// Analytics routes -app.get('/api/analytics/platform', async (req: Request, res: Response) => { - try { - const days = parseInt(req.query.days as string) || 30; - const result = await getPlatformAnalytics(days); - res.json(result); - } catch (error) { - console.error('Analytics error:', error); - res.status(500).json({ error: 'Failed to fetch analytics' }); - } -}); - -app.post('/api/analytics/track', async (req: Request, res: Response) => { - try { - const result = await trackEvent(req.body); - res.json(result); - } catch (error) { - console.error('Track event error:', error); - res.status(500).json({ error: 'Failed to track event' }); - } -}); - -// Collections routes -app.get('/api/collections', async (req: Request, res: Response) => { - try { - const userId = req.headers['x-user-id'] as string; - const result = userId ? await getUserCollections(userId) : await getPublicCollections(); - res.json(result); - } catch (error) { - console.error('Get collections error:', error); - res.status(500).json({ error: 'Failed to fetch collections' }); - } -}); - -// Ratings routes -app.post('/api/ratings', async (req: Request, res: Response) => { - try { - const result = await rateScript(req.body); - res.json(result); - } catch (error) { - console.error('Rate script error:', error); - res.status(500).json({ error: 'Failed to rate script' }); - } -}); - -app.get('/api/scripts/:id/ratings', async (req: Request, res: Response) => { - try { - const result = await getScriptRatingStats(req.params.id); - res.json(result); - } catch (error) { - console.error('Get ratings error:', error); - res.status(500).json({ error: 'Failed to fetch ratings' }); - } -}); - -// Error handling middleware -app.use((error: any, _req: Request, res: Response, _next: NextFunction) => { - console.error('Unhandled error:', error); - res.status(500).json({ error: 'Internal server error' }); -}); - -// 404 handler -app.use('*', (_req: Request, res: Response) => { - res.status(404).json({ error: 'Endpoint not found' }); -}); - -app.listen(PORT, () => { - console.log(`ScriptShare API server running on port ${PORT}`); - console.log(`Environment: ${process.env.NODE_ENV}`); - console.log(`Database URL configured: ${!!process.env.DATABASE_URL}`); -}); +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import { getAllUsers, getUserById } from './lib/api/users.js'; +import { getScripts, getScriptById, createScript } from './lib/api/scripts.js'; +import { login, register } from './lib/api/auth.js'; +import { rateScript, getScriptRatingStats } from './lib/api/ratings.js'; +import { getPlatformAnalytics, trackEvent } from './lib/api/analytics.js'; +import { getUserCollections, getPublicCollections } from './lib/api/collections.js'; + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(cors({ + origin: process.env.CORS_ORIGIN || '*', + credentials: true +})); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true })); + +// Health check endpoint +app.get('/api/health', (_req: Request, res: Response) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Auth routes +app.post('/api/auth/login', async (req: Request, res: Response) => { + try { + const result = await login(req.body); + res.json(result); + } catch (error) { + console.error('Login error:', error); + res.status(401).json({ error: 'Invalid credentials' }); + } +}); + +app.post('/api/auth/register', async (req: Request, res: Response) => { + try { + const result = await register(req.body); + res.json(result); + } catch (error) { + console.error('Register error:', error); + res.status(400).json({ error: 'Registration failed' }); + } +}); + +// Scripts routes +app.get('/api/scripts', async (req: Request, res: Response) => { + try { + const result = await getScripts(req.query); + res.json(result); + } catch (error) { + console.error('Get scripts error:', error); + res.status(500).json({ error: 'Failed to fetch scripts' }); + } +}); + +app.get('/api/scripts/:id', async (req: Request, res: Response) => { + try { + const script = await getScriptById(req.params.id); + if (!script) { + return res.status(404).json({ error: 'Script not found' }); + } + res.json(script); + } catch (error) { + console.error('Get script error:', error); + res.status(500).json({ error: 'Failed to fetch script' }); + } +}); + +app.post('/api/scripts', async (req: Request, res: Response) => { + try { + const userId = req.headers['x-user-id'] as string; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + const result = await createScript(req.body, userId); + res.json(result); + } catch (error) { + console.error('Create script error:', error); + res.status(500).json({ error: 'Failed to create script' }); + } +}); + +// Users routes +app.get('/api/users', async (_req: Request, res: Response) => { + try { + const result = await getAllUsers(); + res.json(result); + } catch (error) { + console.error('Get users error:', error); + res.status(500).json({ error: 'Failed to fetch users' }); + } +}); + +app.get('/api/users/:id', async (req: Request, res: Response) => { + try { + const user = await getUserById(req.params.id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + res.json(user); + } catch (error) { + console.error('Get user error:', error); + res.status(500).json({ error: 'Failed to fetch user' }); + } +}); + +// Analytics routes +app.get('/api/analytics/platform', async (req: Request, res: Response) => { + try { + const days = parseInt(req.query.days as string) || 30; + const result = await getPlatformAnalytics(days); + res.json(result); + } catch (error) { + console.error('Analytics error:', error); + res.status(500).json({ error: 'Failed to fetch analytics' }); + } +}); + +app.post('/api/analytics/track', async (req: Request, res: Response) => { + try { + const result = await trackEvent(req.body); + res.json(result); + } catch (error) { + console.error('Track event error:', error); + res.status(500).json({ error: 'Failed to track event' }); + } +}); + +// Collections routes +app.get('/api/collections', async (req: Request, res: Response) => { + try { + const userId = req.headers['x-user-id'] as string; + const result = userId ? await getUserCollections(userId) : await getPublicCollections(); + res.json(result); + } catch (error) { + console.error('Get collections error:', error); + res.status(500).json({ error: 'Failed to fetch collections' }); + } +}); + +// Ratings routes +app.post('/api/ratings', async (req: Request, res: Response) => { + try { + const result = await rateScript(req.body); + res.json(result); + } catch (error) { + console.error('Rate script error:', error); + res.status(500).json({ error: 'Failed to rate script' }); + } +}); + +app.get('/api/scripts/:id/ratings', async (req: Request, res: Response) => { + try { + const result = await getScriptRatingStats(req.params.id); + res.json(result); + } catch (error) { + console.error('Get ratings error:', error); + res.status(500).json({ error: 'Failed to fetch ratings' }); + } +}); + +// Error handling middleware +app.use((error: any, _req: Request, res: Response, _next: NextFunction) => { + console.error('Unhandled error:', error); + res.status(500).json({ error: 'Internal server error' }); +}); + +// 404 handler +app.use('*', (_req: Request, res: Response) => { + res.status(404).json({ error: 'Endpoint not found' }); +}); + +app.listen(PORT, () => { + console.log(`ScriptShare API server running on port ${PORT}`); + console.log(`Environment: ${process.env.NODE_ENV}`); + console.log(`Database URL configured: ${!!process.env.DATABASE_URL}`); +});