From 4e193ab1b2803d4ba3bd2657f3fe49c604d6d7c6 Mon Sep 17 00:00:00 2001 From: Oliver Gwyther Date: Fri, 22 Aug 2025 16:36:35 +0100 Subject: [PATCH] Refactor Dockerfile and server code to improve TypeScript build process, enhance health check compatibility, and streamline mock API structure. Update hooks for script management to include user ID in create and update operations, ensuring better integration with the API. --- DEPLOYMENT_FIXES.md | 148 ++++++++++------- Dockerfile | 11 +- src/hooks/useScripts.ts | 279 +++++++++++++++---------------- src/server.ts | 358 ++++++++++++++++++++-------------------- 4 files changed, 409 insertions(+), 387 deletions(-) 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}`); +});