Compare commits
2 Commits
a15b87aa5d
...
bdca42213d
Author | SHA1 | Date | |
---|---|---|---|
bdca42213d | |||
4e193ab1b2 |
@ -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! 🎉
|
||||
|
11
Dockerfile
11
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<AuthToken> {\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<AuthToken> {\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<AuthToken> {\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<boolean> {\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
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -8319,16 +8319,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-syntax-highlighter": {
|
||||
"version": "15.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
|
||||
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
|
||||
"version": "15.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.3.tgz",
|
||||
"integrity": "sha512-HebdyA9r20hgmA0q8RyRJ4c/vB4E6KL2HeWb5MNjU3iJEiT2w9jfU2RJsmI6f3Cy3SGE5tm0AIkBzM/E7e9/lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.3.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"highlightjs-vue": "^1.0.0",
|
||||
"lowlight": "^1.17.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^3.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -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, authorId: userId, authorName: data.authorName || 'User'}, 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!');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ export interface ScriptFilters {
|
||||
}
|
||||
|
||||
// Create a new script
|
||||
export async function createScript(data: CreateScriptData) {
|
||||
export async function createScript(data: CreateScriptData, userId?: string) {
|
||||
try {
|
||||
const scriptId = generateId();
|
||||
const now = new Date();
|
||||
|
363
src/server.ts
363
src/server.ts
@ -1,179 +1,184 @@
|
||||
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' });
|
||||
}
|
||||
// Make sure userId is included in the request body
|
||||
const scriptData = {
|
||||
...req.body,
|
||||
authorId: userId
|
||||
};
|
||||
const result = await createScript(scriptData, 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}`);
|
||||
});
|
||||
|
@ -15,10 +15,10 @@
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path mapping */
|
||||
|
Reference in New Issue
Block a user