Add Express API server for Claude API proxy
- Add Express server with vite-express for combined frontend/API serving - Create modular API route structure (server/api/) - Implement Claude API proxy with proper header injection - Support split deployment via API_ONLY and ALLOWED_ORIGINS env vars - Remove Claude proxy from Vite config (now handled by Express) - Add migration plan documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ccdab2780
commit
1152ab0d0c
403
EXPRESS_API_PLAN.md
Normal file
403
EXPRESS_API_PLAN.md
Normal file
@ -0,0 +1,403 @@
|
||||
# Express.js API Server Plan
|
||||
|
||||
## Goal
|
||||
Add an Express.js backend server to handle API routes (starting with Claude API), with support for either combined or split deployment.
|
||||
|
||||
## Advantages Over Next.js Migration
|
||||
- **Minimal frontend changes** - only API URL configuration
|
||||
- **No routing changes** - keep react-router-dom as-is
|
||||
- **Flexible deployment** - combined or split frontend/backend
|
||||
- **Already partially exists** - `server.js` in root has Express + vite-express scaffolding
|
||||
|
||||
## Deployment Options
|
||||
|
||||
### Option A: Combined (Single Server)
|
||||
```
|
||||
Express Server (vite-express)
|
||||
├── Serves static files from dist/
|
||||
└── Handles /api/* routes
|
||||
```
|
||||
- Simpler setup, one deployment
|
||||
- Good for: VPS, Railway, Fly.io, DigitalOcean App Platform
|
||||
|
||||
### Option B: Split (Separate Hosts)
|
||||
```
|
||||
Static Host (CDN) API Server (Node.js)
|
||||
├── Cloudflare Pages ├── Railway
|
||||
├── Netlify ├── Fly.io
|
||||
├── Vercel ├── AWS Lambda
|
||||
└── S3 + CloudFront └── Any VPS
|
||||
|
||||
Serves dist/ Handles /api/*
|
||||
```
|
||||
- Better scalability, cheaper static hosting
|
||||
- Good for: High traffic, global CDN distribution
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### Existing `server.js` (incomplete)
|
||||
```javascript
|
||||
import express from "express";
|
||||
import ViteExpress from "vite-express";
|
||||
import dotenv from "dotenv";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
app.use("/api", expressProxy("local.immersiveidea.com"));
|
||||
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
||||
```
|
||||
|
||||
### Missing Dependencies
|
||||
The following packages are imported but not in package.json:
|
||||
- `express`
|
||||
- `vite-express`
|
||||
- `express-http-proxy`
|
||||
- `dotenv`
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install express vite-express dotenv cors
|
||||
```
|
||||
|
||||
- `express` - Web framework
|
||||
- `vite-express` - Vite integration for combined deployment
|
||||
- `dotenv` - Environment variable loading
|
||||
- `cors` - Cross-origin support for split deployment
|
||||
|
||||
### Phase 2: Create API Routes Structure
|
||||
|
||||
Create a modular API structure:
|
||||
|
||||
```
|
||||
server/
|
||||
├── server.js # Existing WebSocket server (keep as-is)
|
||||
├── api/
|
||||
│ ├── index.js # Main API router
|
||||
│ └── claude.js # Claude API proxy route
|
||||
```
|
||||
|
||||
### Phase 3: Update Root `server.js`
|
||||
|
||||
Replace the current incomplete server.js with:
|
||||
|
||||
```javascript
|
||||
import express from "express";
|
||||
import ViteExpress from "vite-express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import apiRoutes from "./server/api/index.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
|
||||
// CORS configuration for split deployment
|
||||
// In combined mode, same-origin requests don't need CORS
|
||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
|
||||
if (allowedOrigins.length > 0) {
|
||||
app.use(cors({
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
}));
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRoutes);
|
||||
|
||||
// Check if running in API-only mode (split deployment)
|
||||
const apiOnly = process.env.API_ONLY === "true";
|
||||
|
||||
if (apiOnly) {
|
||||
// API-only mode: no static file serving
|
||||
app.listen(process.env.PORT || 3000, () => {
|
||||
console.log(`API server running on port ${process.env.PORT || 3000}`);
|
||||
});
|
||||
} else {
|
||||
// Combined mode: Vite handles static files + SPA
|
||||
ViteExpress.listen(app, process.env.PORT || 3001, () => {
|
||||
console.log(`Server running on port ${process.env.PORT || 3001}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Create API Router
|
||||
|
||||
**`server/api/index.js`**:
|
||||
```javascript
|
||||
import { Router } from "express";
|
||||
import claudeRouter from "./claude.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Claude API proxy
|
||||
router.use("/claude", claudeRouter);
|
||||
|
||||
// Health check
|
||||
router.get("/health", (req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
**`server/api/claude.js`**:
|
||||
```javascript
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
const ANTHROPIC_API_URL = "https://api.anthropic.com";
|
||||
|
||||
router.post("/*", async (req, res) => {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({ error: "API key not configured" });
|
||||
}
|
||||
|
||||
// Get the path after /api/claude (e.g., /v1/messages)
|
||||
const path = req.params[0] || req.path;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
res.status(response.status).json(data);
|
||||
} catch (error) {
|
||||
console.error("Claude API error:", error);
|
||||
res.status(500).json({ error: "Failed to proxy request to Claude API" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
```
|
||||
|
||||
### Phase 5: Update Vite Config
|
||||
|
||||
Remove the Claude proxy from `vite.config.ts` since Express handles it now.
|
||||
|
||||
**Before** (lines 41-56):
|
||||
```javascript
|
||||
'^/api/claude': {
|
||||
target: 'https://api.anthropic.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
const apiKey = env.ANTHROPIC_API_KEY;
|
||||
// ...
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After**: Remove this block entirely. The Express server handles `/api/claude/*`.
|
||||
|
||||
Keep the other proxies (`/sync/*`, `/create-db`, `/api/images`) - they still proxy to deepdiagram.com in dev mode.
|
||||
|
||||
### Phase 6: Add API URL Configuration (for Split Deployment)
|
||||
|
||||
Create a utility to get the API base URL:
|
||||
|
||||
**`src/util/apiConfig.ts`**:
|
||||
```typescript
|
||||
// API base URL - empty string for same-origin (combined deployment)
|
||||
// Set VITE_API_URL for split deployment (e.g., "https://api.yourdomain.com")
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_URL || '';
|
||||
|
||||
export function apiUrl(path: string): string {
|
||||
return `${API_BASE_URL}${path}`;
|
||||
}
|
||||
```
|
||||
|
||||
**Update `src/react/services/diagramAI.ts`**:
|
||||
```typescript
|
||||
import { apiUrl } from '../../util/apiConfig';
|
||||
|
||||
// Change from:
|
||||
const response = await fetch('/api/claude/v1/messages', { ... });
|
||||
|
||||
// To:
|
||||
const response = await fetch(apiUrl('/api/claude/v1/messages'), { ... });
|
||||
```
|
||||
|
||||
This change is backward-compatible:
|
||||
- **Combined deployment**: `VITE_API_URL` is empty, calls go to same origin
|
||||
- **Split deployment**: `VITE_API_URL=https://api.example.com`, calls go to API server
|
||||
|
||||
### Phase 7: Update package.json Scripts
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"build": "node versionBump.js && vite build",
|
||||
"start": "NODE_ENV=production node server.js",
|
||||
"start:api": "API_ONLY=true node server.js",
|
||||
"test": "vitest",
|
||||
"socket": "node server/server.js",
|
||||
"serverBuild": "cd server && tsc"
|
||||
}
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- `dev`: Runs Express + vite-express (serves Vite in dev mode)
|
||||
- `start`: Combined mode - serves dist/ + API
|
||||
- `start:api`: API-only mode for split deployment
|
||||
- Removed `preview` (use `start` instead)
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
| Action | File | Description |
|
||||
|--------|------|-------------|
|
||||
| Modify | `package.json` | Add dependencies, update scripts |
|
||||
| Modify | `server.js` | Full Express server with CORS + API routes |
|
||||
| Create | `server/api/index.js` | Main API router |
|
||||
| Create | `server/api/claude.js` | Claude API proxy endpoint |
|
||||
| Create | `src/util/apiConfig.ts` | API URL configuration utility |
|
||||
| Modify | `src/react/services/diagramAI.ts` | Use apiUrl() for API calls |
|
||||
| Modify | `vite.config.ts` | Remove `/api/claude` proxy block |
|
||||
|
||||
---
|
||||
|
||||
## How vite-express Works
|
||||
|
||||
`vite-express` is a simple integration that:
|
||||
|
||||
1. **Development**: Runs Vite's dev server as middleware, providing HMR
|
||||
2. **Production**: Serves the built `dist/` folder as static files
|
||||
|
||||
This means:
|
||||
- One server handles both API and frontend
|
||||
- No CORS issues (same origin)
|
||||
- HMR works in development
|
||||
- Production-ready with `vite build`
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Option A: Combined Deployment
|
||||
|
||||
Single server handles both frontend and API:
|
||||
|
||||
```bash
|
||||
# Build frontend
|
||||
npm run build
|
||||
|
||||
# Start combined server (serves dist/ + API)
|
||||
npm run start
|
||||
```
|
||||
|
||||
**Environment variables (.env)**:
|
||||
```bash
|
||||
PORT=3001
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
```
|
||||
|
||||
The Express server will:
|
||||
1. Handle `/api/*` routes directly
|
||||
2. Serve static files from `dist/`
|
||||
3. Fall back to `dist/index.html` for SPA routing
|
||||
|
||||
### Option B: Split Deployment
|
||||
|
||||
Separate hosting for frontend (CDN) and API (Node server):
|
||||
|
||||
**API Server:**
|
||||
```bash
|
||||
# Start API-only server
|
||||
npm run start:api
|
||||
```
|
||||
|
||||
**Environment variables (.env for API server)**:
|
||||
```bash
|
||||
PORT=3000
|
||||
API_ONLY=true
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
ALLOWED_ORIGINS=https://your-frontend.com,https://www.your-frontend.com
|
||||
```
|
||||
|
||||
**Frontend (Static Host):**
|
||||
```bash
|
||||
# Build with API URL configured
|
||||
VITE_API_URL=https://api.yourdomain.com npm run build
|
||||
|
||||
# Deploy dist/ to your static host (Cloudflare Pages, Netlify, etc.)
|
||||
```
|
||||
|
||||
**Environment variables (.env.production for frontend build)**:
|
||||
```bash
|
||||
VITE_API_URL=https://api.yourdomain.com
|
||||
```
|
||||
|
||||
### Deployment Examples
|
||||
|
||||
| Deployment | Frontend | API Server | Cost |
|
||||
|------------|----------|------------|------|
|
||||
| Combined | Railway | (same) | ~$5/mo |
|
||||
| Combined | Fly.io | (same) | Free tier |
|
||||
| Split | Cloudflare Pages (free) | Railway ($5/mo) | ~$5/mo |
|
||||
| Split | Netlify (free) | Fly.io (free) | Free |
|
||||
| Split | Vercel (free) | AWS Lambda | Pay-per-use |
|
||||
|
||||
---
|
||||
|
||||
## Future API Routes
|
||||
|
||||
To add more API routes, create new files in `server/api/`:
|
||||
|
||||
```javascript
|
||||
// server/api/index.js
|
||||
import claudeRouter from "./claude.js";
|
||||
import imagesRouter from "./images.js"; // future
|
||||
import authRouter from "./auth.js"; // future
|
||||
|
||||
router.use("/claude", claudeRouter);
|
||||
router.use("/images", imagesRouter);
|
||||
router.use("/auth", authRouter);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Order
|
||||
|
||||
1. `npm install express vite-express dotenv cors`
|
||||
2. Create `server/api/index.js`
|
||||
3. Create `server/api/claude.js`
|
||||
4. Create `src/util/apiConfig.ts`
|
||||
5. Update `src/react/services/diagramAI.ts` to use `apiUrl()`
|
||||
6. Update `server.js` (root) with full Express + CORS setup
|
||||
7. Remove `/api/claude` proxy from `vite.config.ts`
|
||||
8. Update `package.json` scripts
|
||||
9. Test combined: `npm run dev` and verify Claude API works
|
||||
10. (Optional) Test split: Set `VITE_API_URL` and `API_ONLY=true`
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **WebSocket server unchanged**: `server/server.js` (port 8080) runs separately
|
||||
- **Minimal frontend changes**: Only `diagramAI.ts` updated to use `apiUrl()`
|
||||
- **Environment variables**: `ANTHROPIC_API_KEY` already in `.env.local`
|
||||
- **Node version**: Requires Node 18+ for native `fetch`
|
||||
- **CORS**: Only enabled when `ALLOWED_ORIGINS` is set (split deployment)
|
||||
- **Backward compatible**: Works as combined deployment by default
|
||||
167
NEXT_MIGRATION_PLAN.md
Normal file
167
NEXT_MIGRATION_PLAN.md
Normal file
@ -0,0 +1,167 @@
|
||||
# Vite to Next.js Migration Plan
|
||||
|
||||
## Goal
|
||||
Migrate from Vite to Next.js App Router to get proper API route support, with minimal changes to existing code.
|
||||
|
||||
## Configuration
|
||||
- **Router**: App Router with `'use client'` on all pages
|
||||
- **Rendering**: CSR only (no SSR) - simplifies migration since BabylonJS can't SSR
|
||||
- **API Routes**: Claude API now, structured for future expansion
|
||||
- **External Proxies**: Keep sync/create-db/images as Next.js rewrites to deepdiagram.com
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup (No Breaking Changes)
|
||||
|
||||
### 1.1 Install Next.js
|
||||
```bash
|
||||
npm install next
|
||||
```
|
||||
|
||||
### 1.2 Create `next.config.js`
|
||||
```javascript
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
return [
|
||||
{ source: '/sync/:path*', destination: 'https://www.deepdiagram.com/sync/:path*' },
|
||||
{ source: '/create-db', destination: 'https://www.deepdiagram.com/create-db' },
|
||||
{ source: '/api/images', destination: 'https://www.deepdiagram.com/api/images' },
|
||||
];
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
config.experiments = { ...config.experiments, asyncWebAssembly: true };
|
||||
return config;
|
||||
},
|
||||
};
|
||||
module.exports = nextConfig;
|
||||
```
|
||||
|
||||
### 1.3 Update `tsconfig.json`
|
||||
Add path alias:
|
||||
```json
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["./*"] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Create New Files
|
||||
|
||||
### 2.1 `src/react/providers.tsx` (extract from webApp.tsx)
|
||||
- Move Auth0Provider and FeatureProvider wrapping here
|
||||
- Add `'use client'` directive
|
||||
- Handle window/document checks for SSR safety
|
||||
|
||||
### 2.2 `app/layout.tsx`
|
||||
- Root layout with html/body tags
|
||||
- Metadata (title, favicon from current index.html)
|
||||
- Import global CSS
|
||||
|
||||
### 2.3 `app/globals.css`
|
||||
```css
|
||||
@import '../src/react/styles.css';
|
||||
@import '@mantine/core/styles.css';
|
||||
```
|
||||
|
||||
### 2.4 `app/api/claude/[...path]/route.ts`
|
||||
- POST handler that proxies to api.anthropic.com
|
||||
- Injects `ANTHROPIC_API_KEY` from env
|
||||
- Adds `x-api-key` and `anthropic-version` headers
|
||||
|
||||
### 2.5 Page files (all with `'use client'`)
|
||||
| Route | File | Component |
|
||||
|-------|------|-----------|
|
||||
| `/` | `app/page.tsx` | About |
|
||||
| `/documentation` | `app/documentation/page.tsx` | Documentation |
|
||||
| `/examples` | `app/examples/page.tsx` | Examples |
|
||||
| `/pricing` | `app/pricing/page.tsx` | Pricing |
|
||||
| `/db/[visibility]/[db]` | `app/db/[visibility]/[db]/page.tsx` | VrExperience |
|
||||
| 404 | `app/not-found.tsx` | NotFound |
|
||||
|
||||
### 2.6 `src/react/components/ProtectedPage.tsx`
|
||||
- Next.js version of route protection
|
||||
- Uses `useRouter` from `next/navigation` for redirects
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Modify Existing Files
|
||||
|
||||
### 3.1 `src/react/pages/vrExperience.tsx`
|
||||
**Changes:**
|
||||
- Remove `useParams()` from react-router-dom
|
||||
- Accept `visibility` and `db` as props instead
|
||||
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
|
||||
|
||||
### 3.2 `src/react/pageHeader.tsx`
|
||||
**Changes:**
|
||||
- Replace `import {Link} from "react-router-dom"` with `import Link from "next/link"`
|
||||
- Change `to={item.href}` to `href={item.href}` on Link components
|
||||
|
||||
### 3.3 `src/react/marketing/about.tsx`
|
||||
**Changes:**
|
||||
- Replace `useNavigate()` with `useRouter()` from `next/navigation`
|
||||
- Change `navigate('/path')` to `router.push('/path')`
|
||||
|
||||
### 3.4 `package.json`
|
||||
```json
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "node versionBump.js && next build",
|
||||
"start": "next start -p 3001",
|
||||
"test": "vitest",
|
||||
"socket": "node server/server.js",
|
||||
"serverBuild": "cd server && tsc"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Delete Old Files
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `vite.config.ts` | Replaced by next.config.js |
|
||||
| `index.html` | Next.js generates HTML |
|
||||
| `src/webApp.ts` | Entry point no longer needed |
|
||||
| `src/react/webRouter.tsx` | Replaced by app/ routing |
|
||||
| `src/react/webApp.tsx` | Logic moved to providers.tsx |
|
||||
| `src/react/components/ProtectedRoute.tsx` | Replaced by ProtectedPage.tsx |
|
||||
|
||||
---
|
||||
|
||||
## Critical Files to Modify
|
||||
|
||||
- `src/react/pages/vrExperience.tsx` - useParams -> props
|
||||
- `src/react/pageHeader.tsx` - react-router Link -> Next.js Link
|
||||
- `src/react/marketing/about.tsx` - useNavigate -> useRouter
|
||||
- `src/react/webApp.tsx` - extract to providers.tsx
|
||||
- `package.json` - scripts update
|
||||
- `tsconfig.json` - path aliases
|
||||
|
||||
---
|
||||
|
||||
## Migration Order
|
||||
|
||||
1. Install next, create next.config.js
|
||||
2. Update tsconfig.json
|
||||
3. Create app/globals.css
|
||||
4. Create src/react/providers.tsx
|
||||
5. Create app/layout.tsx
|
||||
6. Create app/api/claude/[...path]/route.ts
|
||||
7. Create src/react/components/ProtectedPage.tsx
|
||||
8. Modify vrExperience.tsx (accept props)
|
||||
9. Create all app/*/page.tsx files
|
||||
10. Modify pageHeader.tsx (Next.js Link)
|
||||
11. Modify about.tsx (useRouter)
|
||||
12. Update package.json scripts
|
||||
13. Delete old files (vite.config.ts, index.html, webApp.ts, webRouter.tsx, webApp.tsx)
|
||||
14. Test all routes
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Havok WASM**: Move `HavokPhysics.wasm` to `public/` folder
|
||||
- **react-router-dom**: Can be removed from dependencies after migration
|
||||
- **vite devDependencies**: Can be removed (vite, vite-plugin-cp)
|
||||
1006
package-lock.json
generated
1006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -8,12 +8,12 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "node server.js",
|
||||
"test": "vitest",
|
||||
"build": "node versionBump.js && vite build",
|
||||
"preview": "vite preview",
|
||||
"start": "NODE_ENV=production node server.js",
|
||||
"start:api": "API_ONLY=true node server.js",
|
||||
"socket": "node server/server.js",
|
||||
"serve": "node server.js",
|
||||
"serverBuild": "cd server && tsc",
|
||||
"havok": "cp ./node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm ./node_modules/.vite/deps"
|
||||
},
|
||||
@ -27,38 +27,42 @@
|
||||
"@babylonjs/materials": "^8.16.2",
|
||||
"@babylonjs/serializers": "^8.16.2",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@giphy/js-fetch-api": "^5.6.0",
|
||||
"@giphy/react-components": "^9.6.0",
|
||||
"@mantine/core": "^7.17.8",
|
||||
"@mantine/form": "^7.17.8",
|
||||
"@mantine/hooks": "^7.17.8",
|
||||
"@giphy/react-components": "^9.6.0",
|
||||
"@giphy/js-fetch-api": "^5.6.0",
|
||||
"@maptiler/client": "1.8.1",
|
||||
"@picovoice/cobra-web": "^2.0.3",
|
||||
"@picovoice/eagle-web": "^1.0.0",
|
||||
"@picovoice/web-voice-processor": "^4.0.9",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/react": "^18.2.72",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"axios": "^1.10.0",
|
||||
"canvas-hypertxt": "1.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"events": "^3.3.0",
|
||||
"express": "^5.2.1",
|
||||
"hash-wasm": "4.11.0",
|
||||
"hls.js": "^1.1.4",
|
||||
"js-crypto-aes": "1.0.6",
|
||||
"loglevel": "^1.9.1",
|
||||
"meaningful-string": "^1.4.0",
|
||||
"peer-lite": "2.0.2",
|
||||
"use-pouchdb": "^2.0.2",
|
||||
"pouchdb": "^8.0.1",
|
||||
"pouchdb-find": "^8.0.1",
|
||||
"query-string": "^8.1.0",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"@tabler/icons-react": "^3.14.0",
|
||||
"recordrtc": "^5.6.0",
|
||||
"rfc4648": "^1.5.3",
|
||||
"round": "^2.0.1",
|
||||
"uint8-to-b64": "^1.0.2",
|
||||
"use-pouchdb": "^2.0.2",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-express": "^0.21.1",
|
||||
"websocket": "^1.0.34",
|
||||
"websocket-ts": "^2.1.5"
|
||||
},
|
||||
@ -69,4 +73,4 @@
|
||||
"vite-plugin-cp": "^1.0.0",
|
||||
"vitest": "^1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
38
server.js
38
server.js
@ -1,13 +1,41 @@
|
||||
import express from "express";
|
||||
import ViteExpress from "vite-express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import expressProxy from "express-http-proxy";
|
||||
import apiRoutes from "./server/api/index.js";
|
||||
|
||||
// Load .env.local first, then fall back to .env
|
||||
dotenv.config({ path: '.env.local' });
|
||||
dotenv.config();
|
||||
|
||||
|
||||
|
||||
const app = express();
|
||||
app.use("/api", expressProxy("local.immersiveidea.com"));
|
||||
|
||||
ViteExpress.listen(app, process.env.PORT || 3001, () => console.log("Server is listening..."));
|
||||
// CORS configuration for split deployment
|
||||
// In combined mode, same-origin requests don't need CORS
|
||||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [];
|
||||
if (allowedOrigins.length > 0) {
|
||||
app.use(cors({
|
||||
origin: allowedOrigins,
|
||||
credentials: true,
|
||||
}));
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRoutes);
|
||||
|
||||
// Check if running in API-only mode (split deployment)
|
||||
const apiOnly = process.env.API_ONLY === "true";
|
||||
|
||||
if (apiOnly) {
|
||||
// API-only mode: no static file serving
|
||||
app.listen(process.env.PORT || 3000, () => {
|
||||
console.log(`API server running on port ${process.env.PORT || 3000}`);
|
||||
});
|
||||
} else {
|
||||
// Combined mode: Vite handles static files + SPA
|
||||
ViteExpress.listen(app, process.env.PORT || 3001, () => {
|
||||
console.log(`Server running on port ${process.env.PORT || 3001}`);
|
||||
});
|
||||
}
|
||||
|
||||
38
server/api/claude.js
Normal file
38
server/api/claude.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
const ANTHROPIC_API_URL = "https://api.anthropic.com";
|
||||
|
||||
// Express 5 uses named parameters for wildcards
|
||||
router.post("/*path", async (req, res) => {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(500).json({ error: "API key not configured" });
|
||||
}
|
||||
|
||||
// Get the path after /api/claude (e.g., /v1/messages)
|
||||
// Express 5 returns path segments as an array
|
||||
const pathParam = req.params.path;
|
||||
const path = "/" + (Array.isArray(pathParam) ? pathParam.join("/") : pathParam || "");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ANTHROPIC_API_URL}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
res.status(response.status).json(data);
|
||||
} catch (error) {
|
||||
console.error("Claude API error:", error.message);
|
||||
res.status(500).json({ error: "Failed to proxy request to Claude API" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
14
server/api/index.js
Normal file
14
server/api/index.js
Normal file
@ -0,0 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import claudeRouter from "./claude.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Claude API proxy
|
||||
router.use("/claude", claudeRouter);
|
||||
|
||||
// Health check
|
||||
router.get("/health", (req, res) => {
|
||||
res.json({ status: "ok" });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -39,23 +39,8 @@ export default defineConfig(({mode}) => {
|
||||
'^/api/images': {
|
||||
target: 'https://www.deepdiagram.com/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/api/claude': {
|
||||
target: 'https://api.anthropic.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
|
||||
const apiKey = env.ANTHROPIC_API_KEY;
|
||||
console.log(` API KEY: ${apiKey}`);
|
||||
if (apiKey) {
|
||||
proxyReq.setHeader('x-api-key', apiKey);
|
||||
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// /api/claude is now handled by Express server
|
||||
}
|
||||
|
||||
},
|
||||
@ -73,22 +58,8 @@ export default defineConfig(({mode}) => {
|
||||
'^/api/images': {
|
||||
target: 'https://www.deepdiagram.com/',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/api/claude': {
|
||||
target: 'https://api.anthropic.com',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api\/claude/, ''),
|
||||
configure: (proxy) => {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
const apiKey = env.ANTHROPIC_API_KEY;
|
||||
console.log(` API KEY: ${apiKey}`);
|
||||
if (apiKey) {
|
||||
proxyReq.setHeader('x-api-key', apiKey);
|
||||
proxyReq.setHeader('anthropic-version', '2023-06-01');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// /api/claude is now handled by Express server
|
||||
}
|
||||
},
|
||||
base: "/"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user