mirror of
https://github.com/LukeHagar/relay.git
synced 2025-12-06 12:47:49 +00:00
Refactor SvelteKit app to unified webhook relay with WebSocket support
Co-authored-by: lukeslakemail <lukeslakemail@gmail.com>
This commit is contained in:
@@ -7,10 +7,12 @@ GITHUB_ID="your-github-oauth-app-id"
|
|||||||
GITHUB_SECRET="your-github-oauth-app-secret"
|
GITHUB_SECRET="your-github-oauth-app-secret"
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
|
|
||||||
# Backend Servers
|
# Webhook Configuration
|
||||||
INGEST_SERVER_URL="http://localhost:4000"
|
WEBHOOK_DOMAIN="yourdomain.com"
|
||||||
RELAY_SERVER_URL="http://localhost:4200"
|
WEBHOOK_SUBDOMAIN_PATTERN="*.yourdomain.com"
|
||||||
WEBSOCKET_URL="ws://localhost:4200/api/relay"
|
|
||||||
|
# Optional: Custom webhook path prefix
|
||||||
|
# WEBHOOK_PATH_PREFIX="/webhook"
|
||||||
|
|
||||||
# Optional: Custom domain for production
|
# Optional: Custom domain for production
|
||||||
# DOMAIN="yourdomain.com"
|
# DOMAIN="yourdomain.com"
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
# Webhook Relay - SvelteKit Integration
|
# Webhook Relay - SvelteKit Fullstack Application
|
||||||
|
|
||||||
A fullstack SvelteKit application that provides a modern web interface for the webhook relay system. This application integrates with the existing Bun-based webhook relay backend to provide real-time webhook management, forwarding, and monitoring.
|
A complete webhook relay system built entirely with SvelteKit, providing webhook ingestion, relay functionality, and a modern web interface all in one application.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🚀 Real-time Webhook Management
|
### 🚀 Complete Webhook Management
|
||||||
- **Live Dashboard**: Real-time webhook event monitoring with WebSocket connections
|
- **Webhook Ingestion**: Receive webhooks via subdomain routing
|
||||||
|
- **Real-time Dashboard**: Live webhook event monitoring with WebSocket connections
|
||||||
- **Event History**: Complete webhook event history with search and filtering
|
- **Event History**: Complete webhook event history with search and filtering
|
||||||
- **Event Details**: View full request bodies, headers, and metadata
|
- **Relay System**: Forward webhooks to multiple configured targets
|
||||||
|
|
||||||
### 🔄 Webhook Relay System
|
### 🔄 Webhook Relay System
|
||||||
- **Multiple Targets**: Configure multiple forwarding destinations
|
- **Multiple Targets**: Configure multiple forwarding destinations
|
||||||
- **Target Management**: Add, edit, delete, and toggle relay targets
|
- **Target Management**: Add, edit, delete, and toggle relay targets
|
||||||
- **Automatic Forwarding**: Incoming webhooks are automatically forwarded to all active targets
|
- **Automatic Forwarding**: Incoming webhooks are automatically forwarded to all active targets
|
||||||
|
- **Relay Analytics**: Track success rates and response times
|
||||||
|
|
||||||
### 🔐 Authentication & Security
|
### 🔐 Authentication & Security
|
||||||
- **GitHub OAuth**: Secure authentication using GitHub
|
- **GitHub OAuth**: Secure authentication using GitHub
|
||||||
@@ -26,38 +28,39 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Frontend (SvelteKit)
|
### Single SvelteKit Application
|
||||||
- **Port**: 3000
|
- **Webhook Ingestion**: `/webhook/[...path]` - Handles all incoming webhooks
|
||||||
- **Framework**: SvelteKit with TypeScript
|
- **WebSocket Server**: `/api/ws` - Real-time updates for authenticated users
|
||||||
- **Styling**: Tailwind CSS
|
- **Web Interface**: Modern dashboard and management UI
|
||||||
- **Icons**: Lucide Svelte
|
|
||||||
- **State Management**: Svelte stores for real-time updates
|
|
||||||
|
|
||||||
### Backend Integration
|
|
||||||
- **Ingest Server**: Receives webhooks via subdomains (port 4000)
|
|
||||||
- **Relay Server**: Handles authentication and WebSocket connections (port 4200)
|
|
||||||
- **Database**: PostgreSQL with Prisma ORM
|
- **Database**: PostgreSQL with Prisma ORM
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
1. **WebSocket Client** (`src/lib/websocket.ts`)
|
1. **Webhook Handler** (`src/routes/webhook/[...path]/+server.ts`)
|
||||||
- Manages real-time connections to the relay server
|
- Receives webhooks via subdomain routing
|
||||||
- Handles automatic reconnection
|
- Stores events in database
|
||||||
- Provides reactive stores for webhook events
|
- Forwards to configured relay targets
|
||||||
|
- Broadcasts real-time updates
|
||||||
|
|
||||||
2. **Authentication** (`src/lib/auth.ts`)
|
2. **WebSocket Handler** (`src/routes/api/ws/+server.ts`)
|
||||||
|
- Manages authenticated WebSocket connections
|
||||||
|
- Provides real-time webhook event updates
|
||||||
|
- Handles connection lifecycle
|
||||||
|
|
||||||
|
3. **Relay Service** (`src/lib/relay.ts`)
|
||||||
|
- Forwards webhooks to configured targets
|
||||||
|
- Tracks relay success/failure
|
||||||
|
- Handles timeouts and errors
|
||||||
|
|
||||||
|
4. **Authentication** (`src/lib/auth.ts`)
|
||||||
- GitHub OAuth integration
|
- GitHub OAuth integration
|
||||||
- Session management with Auth.js
|
- Session management
|
||||||
- User data persistence
|
- User data persistence
|
||||||
|
|
||||||
3. **Database Layer** (`src/lib/db.ts`)
|
|
||||||
- Prisma client configuration
|
|
||||||
- Connection pooling and optimization
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js 18+ or Bun
|
- Node.js 18+
|
||||||
- PostgreSQL database
|
- PostgreSQL database
|
||||||
- GitHub OAuth application
|
- GitHub OAuth application
|
||||||
|
|
||||||
@@ -81,6 +84,7 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
GITHUB_ID="your-github-oauth-app-id"
|
GITHUB_ID="your-github-oauth-app-id"
|
||||||
GITHUB_SECRET="your-github-oauth-app-secret"
|
GITHUB_SECRET="your-github-oauth-app-secret"
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
|
WEBHOOK_DOMAIN="yourdomain.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up the database**:
|
3. **Set up the database**:
|
||||||
@@ -94,15 +98,6 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Start the backend servers** (in separate terminals):
|
|
||||||
```bash
|
|
||||||
# Terminal 1 - Ingest Server
|
|
||||||
bun run server/index.ts
|
|
||||||
|
|
||||||
# Terminal 2 - Relay Server
|
|
||||||
bun run server/relay.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
1. **Access the application**: http://localhost:3000
|
1. **Access the application**: http://localhost:3000
|
||||||
@@ -111,10 +106,36 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
4. **Add relay targets**: Configure where webhooks should be forwarded
|
4. **Add relay targets**: Configure where webhooks should be forwarded
|
||||||
5. **Monitor webhooks**: View real-time webhook events on the dashboard
|
5. **Monitor webhooks**: View real-time webhook events on the dashboard
|
||||||
|
|
||||||
|
## Webhook Flow
|
||||||
|
|
||||||
|
### 1. Webhook Reception
|
||||||
|
```
|
||||||
|
External Service → https://{subdomain}.yourdomain.com/webhook → SvelteKit Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Processing Pipeline
|
||||||
|
1. **Subdomain Extraction**: Extract user subdomain from hostname
|
||||||
|
2. **User Validation**: Verify subdomain belongs to authenticated user
|
||||||
|
3. **Event Storage**: Store webhook data in database
|
||||||
|
4. **Target Relay**: Forward to all active relay targets
|
||||||
|
5. **Real-time Broadcast**: Send updates to connected WebSocket clients
|
||||||
|
|
||||||
|
### 3. Real-time Updates
|
||||||
|
```
|
||||||
|
Webhook Event → Database → WebSocket Broadcast → SvelteKit UI Updates
|
||||||
|
```
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
### Webhook Statistics
|
### Webhook Ingestion
|
||||||
|
- `ANY /webhook/[...path]` - Receive webhooks (subdomain-based routing)
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
- `GET /api/ws` - WebSocket connection for real-time updates
|
||||||
|
|
||||||
|
### Webhook Management
|
||||||
- `GET /api/webhooks/stats` - Get webhook statistics for dashboard
|
- `GET /api/webhooks/stats` - Get webhook statistics for dashboard
|
||||||
|
- `POST /api/test-webhook` - Create test webhook events
|
||||||
|
|
||||||
### Relay Targets
|
### Relay Targets
|
||||||
- `POST /api/targets` - Create a new relay target
|
- `POST /api/targets` - Create a new relay target
|
||||||
@@ -122,13 +143,8 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
- `DELETE /api/targets/[id]` - Delete a relay target
|
- `DELETE /api/targets/[id]` - Delete a relay target
|
||||||
- `PUT /api/targets/[id]/toggle` - Toggle target active status
|
- `PUT /api/targets/[id]/toggle` - Toggle target active status
|
||||||
|
|
||||||
## Webhook Flow
|
### User Settings
|
||||||
|
- `PUT /api/settings/subdomain` - Update user subdomain
|
||||||
1. **Webhook Reception**: Incoming webhooks are received at `{subdomain}.yourdomain.com`
|
|
||||||
2. **Event Storage**: Webhook data is stored in the database
|
|
||||||
3. **Real-time Updates**: WebSocket clients receive immediate updates
|
|
||||||
4. **Target Forwarding**: Webhooks are forwarded to all active relay targets
|
|
||||||
5. **UI Updates**: SvelteKit interface updates in real-time
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -136,73 +152,120 @@ A fullstack SvelteKit application that provides a modern web interface for the w
|
|||||||
```
|
```
|
||||||
sveltekit-app/
|
sveltekit-app/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── lib/ # Shared utilities and configurations
|
│ ├── lib/ # Shared utilities and services
|
||||||
│ ├── routes/ # SvelteKit routes and API endpoints
|
│ │ ├── auth.ts # Authentication configuration
|
||||||
│ └── app.css # Global styles
|
│ │ ├── db.ts # Database connection
|
||||||
├── prisma/ # Database schema and migrations
|
│ │ ├── relay.ts # Webhook relay service
|
||||||
├── static/ # Static assets
|
│ │ └── websocket.ts # WebSocket client utilities
|
||||||
└── package.json # Dependencies and scripts
|
│ ├── routes/
|
||||||
|
│ │ ├── webhook/ # Webhook ingestion endpoints
|
||||||
|
│ │ ├── api/ # API endpoints
|
||||||
|
│ │ ├── +page.svelte # Dashboard
|
||||||
|
│ │ ├── webhooks/ # Webhook management
|
||||||
|
│ │ ├── targets/ # Relay target management
|
||||||
|
│ │ └── settings/ # User settings
|
||||||
|
│ └── app.css # Global styles
|
||||||
|
├── prisma/ # Database schema and migrations
|
||||||
|
└── package.json # Dependencies and scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Technologies
|
### Key Technologies
|
||||||
- **SvelteKit**: Fullstack framework for the web interface
|
- **SvelteKit**: Fullstack framework for web interface and API
|
||||||
- **TypeScript**: Type-safe development
|
- **TypeScript**: Type-safe development
|
||||||
- **Tailwind CSS**: Utility-first styling
|
- **Tailwind CSS**: Utility-first styling
|
||||||
- **Prisma**: Database ORM and migrations
|
- **Prisma**: Database ORM and migrations
|
||||||
- **Auth.js**: Authentication and session management
|
- **Auth.js**: Authentication and session management
|
||||||
- **WebSocket**: Real-time communication
|
- **WebSocket**: Real-time communication
|
||||||
|
|
||||||
### Customization
|
## Production Deployment
|
||||||
|
|
||||||
#### Adding New Webhook Providers
|
### 1. Build the Application
|
||||||
1. Extend the webhook event schema in `prisma/schema.prisma`
|
```bash
|
||||||
2. Add provider-specific parsing in the ingest server
|
npm run build
|
||||||
3. Update the UI components to display new fields
|
```
|
||||||
|
|
||||||
#### Custom Relay Logic
|
### 2. Environment Configuration
|
||||||
1. Modify the relay server to add custom forwarding logic
|
|
||||||
2. Add conditional forwarding based on webhook content
|
|
||||||
3. Implement retry mechanisms and error handling
|
|
||||||
|
|
||||||
#### UI Enhancements
|
|
||||||
1. Add new dashboard widgets for specific metrics
|
|
||||||
2. Create custom event visualizations
|
|
||||||
3. Implement advanced filtering and search
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Production Setup
|
|
||||||
1. **Build the application**:
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set up production environment**:
|
|
||||||
- Configure production database
|
|
||||||
- Set up proper domain and SSL certificates
|
|
||||||
- Configure reverse proxy for subdomain routing
|
|
||||||
|
|
||||||
3. **Deploy with adapter**:
|
|
||||||
```bash
|
|
||||||
npm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Variables for Production
|
|
||||||
```env
|
```env
|
||||||
DATABASE_URL="postgresql://..."
|
DATABASE_URL="postgresql://..."
|
||||||
AUTH_SECRET="production-secret"
|
AUTH_SECRET="production-secret"
|
||||||
GITHUB_ID="production-github-id"
|
GITHUB_ID="production-github-id"
|
||||||
GITHUB_SECRET="production-github-secret"
|
GITHUB_SECRET="production-github-secret"
|
||||||
REDIRECT_URL="https://yourdomain.com"
|
REDIRECT_URL="https://yourdomain.com"
|
||||||
|
WEBHOOK_DOMAIN="yourdomain.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
### 3. Domain Configuration
|
||||||
|
- Set up wildcard DNS records (`*.yourdomain.com`)
|
||||||
|
- Configure reverse proxy (nginx/traefik) for subdomain routing
|
||||||
|
- Set up SSL certificates for secure webhook reception
|
||||||
|
|
||||||
1. Fork the repository
|
### 4. Start Production Server
|
||||||
2. Create a feature branch
|
```bash
|
||||||
3. Make your changes
|
npm run preview
|
||||||
4. Add tests if applicable
|
```
|
||||||
5. Submit a pull request
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Webhook Creation
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/test-webhook \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"subdomain": "your-subdomain", "method": "POST", "path": "/test", "body": {"test": "data"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send Webhook to Your Endpoint
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-subdomain.yourdomain.com/webhook/test \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"event": "test", "data": "example"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 1. Authentication & Authorization
|
||||||
|
- GitHub OAuth for user authentication
|
||||||
|
- Session-based authentication with secure cookies
|
||||||
|
- User data isolation in database queries
|
||||||
|
- CSRF protection via SvelteKit
|
||||||
|
|
||||||
|
### 2. Data Protection
|
||||||
|
- Input validation on all endpoints
|
||||||
|
- SQL injection prevention via Prisma ORM
|
||||||
|
- XSS protection via SvelteKit's built-in sanitization
|
||||||
|
- Secure WebSocket connections
|
||||||
|
|
||||||
|
### 3. Rate Limiting
|
||||||
|
- Consider implementing rate limiting for webhook endpoints
|
||||||
|
- WebSocket connection limits
|
||||||
|
- Database query optimization
|
||||||
|
|
||||||
|
## Monitoring & Analytics
|
||||||
|
|
||||||
|
### 1. Real-time Metrics
|
||||||
|
- WebSocket connection status
|
||||||
|
- Webhook event counts
|
||||||
|
- Relay target success rates
|
||||||
|
- User activity tracking
|
||||||
|
|
||||||
|
### 2. Error Tracking
|
||||||
|
- WebSocket connection failures
|
||||||
|
- API endpoint errors
|
||||||
|
- Database connection issues
|
||||||
|
- Relay target failures
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### 1. Advanced Features
|
||||||
|
- **Webhook Templates**: Pre-configured webhook formats
|
||||||
|
- **Conditional Relay**: Forward based on webhook content
|
||||||
|
- **Retry Mechanisms**: Automatic retry for failed relays
|
||||||
|
- **Webhook Signatures**: Security verification
|
||||||
|
|
||||||
|
### 2. Enterprise Features
|
||||||
|
- **Team Management**: Multi-user organizations
|
||||||
|
- **API Keys**: Programmatic access
|
||||||
|
- **Audit Logs**: Complete activity tracking
|
||||||
|
- **Advanced Analytics**: Detailed performance metrics
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-node": "^3.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
|||||||
127
sveltekit-app/src/lib/relay.ts
Normal file
127
sveltekit-app/src/lib/relay.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { prisma } from '$lib/db';
|
||||||
|
|
||||||
|
export interface RelayResult {
|
||||||
|
success: boolean;
|
||||||
|
target: string;
|
||||||
|
statusCode?: number;
|
||||||
|
error?: string;
|
||||||
|
responseTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function relayWebhookToTargets(
|
||||||
|
userId: string,
|
||||||
|
webhookData: {
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
query: string;
|
||||||
|
body: any;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
}
|
||||||
|
): Promise<RelayResult[]> {
|
||||||
|
try {
|
||||||
|
// Get all active relay targets for the user
|
||||||
|
const targets = await prisma.relayTarget.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targets.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward webhook to all active targets
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
targets.map(async (target) => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prepare headers for forwarding
|
||||||
|
const forwardHeaders: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'WebhookRelay/1.0',
|
||||||
|
'X-Webhook-Relay-Source': 'webhook-relay',
|
||||||
|
'X-Webhook-Relay-Target': target.nickname || target.id,
|
||||||
|
...webhookData.headers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove headers that shouldn't be forwarded
|
||||||
|
delete forwardHeaders['host'];
|
||||||
|
delete forwardHeaders['authorization'];
|
||||||
|
delete forwardHeaders['cookie'];
|
||||||
|
|
||||||
|
// Forward the webhook
|
||||||
|
const response = await fetch(target.target, {
|
||||||
|
method: webhookData.method,
|
||||||
|
headers: forwardHeaders,
|
||||||
|
body: webhookData.method !== 'GET' ? JSON.stringify(webhookData.body) : undefined,
|
||||||
|
signal: AbortSignal.timeout(30000) // 30 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: response.ok,
|
||||||
|
target: target.target,
|
||||||
|
statusCode: response.status,
|
||||||
|
responseTime
|
||||||
|
} as RelayResult;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
target: target.target,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
responseTime
|
||||||
|
} as RelayResult;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process results
|
||||||
|
return results.map((result, index) => {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
return result.value;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
target: targets[index]?.target || 'unknown',
|
||||||
|
error: result.reason?.message || 'Unknown error'
|
||||||
|
} as RelayResult;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error relaying webhook to targets:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Store relay results for analytics
|
||||||
|
export async function storeRelayResults(
|
||||||
|
webhookEventId: string,
|
||||||
|
results: RelayResult[]
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// You could create a new table for relay results if needed
|
||||||
|
// For now, we'll just log them
|
||||||
|
console.log('Relay results for webhook:', webhookEventId, results);
|
||||||
|
|
||||||
|
// Example of storing results (if you add a RelayResult table):
|
||||||
|
// await prisma.relayResult.createMany({
|
||||||
|
// data: results.map(result => ({
|
||||||
|
// webhookEventId,
|
||||||
|
// target: result.target,
|
||||||
|
// success: result.success,
|
||||||
|
// statusCode: result.statusCode,
|
||||||
|
// error: result.error,
|
||||||
|
// responseTime: result.responseTime
|
||||||
|
// }))
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error storing relay results:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,6 +136,9 @@ class WebSocketClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createWebSocketClient(url: string) {
|
export function createWebSocketClient() {
|
||||||
return new WebSocketClient(url);
|
// Use the SvelteKit WebSocket endpoint
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const host = window.location.host;
|
||||||
|
return new WebSocketClient(`${protocol}//${host}/api/ws`);
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
// Connect to WebSocket for real-time updates
|
// Connect to SvelteKit WebSocket endpoint
|
||||||
wsClient = createWebSocketClient(`ws://localhost:4200/api/relay`);
|
wsClient = createWebSocketClient();
|
||||||
|
|
||||||
wsClient.events.subscribe((newEvents) => {
|
wsClient.events.subscribe((newEvents) => {
|
||||||
events = newEvents;
|
events = newEvents;
|
||||||
|
|||||||
63
sveltekit-app/src/routes/api/test-webhook/+server.ts
Normal file
63
sveltekit-app/src/routes/api/test-webhook/+server.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { prisma } from '$lib/db';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
const session = await locals.getSession();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { subdomain, method = 'POST', path = '/test', body = { test: true } } = await request.json();
|
||||||
|
|
||||||
|
if (!subdomain) {
|
||||||
|
return json({ error: 'Subdomain is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the subdomain belongs to the user
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
subdomain: subdomain
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ error: 'Invalid subdomain for user' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test webhook event
|
||||||
|
const webhookEvent = {
|
||||||
|
userId: user.id,
|
||||||
|
method: method,
|
||||||
|
path: path,
|
||||||
|
query: '',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: JSON.stringify({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'Test-Webhook/1.0',
|
||||||
|
'X-Test-Webhook': 'true'
|
||||||
|
}),
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
const storedEvent = await prisma.webhookEvent.create({
|
||||||
|
data: webhookEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
message: 'Test webhook created successfully',
|
||||||
|
eventId: storedEvent.id,
|
||||||
|
webhookUrl: `https://${subdomain}.yourdomain.com${path}`,
|
||||||
|
timestamp: storedEvent.createdAt
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating test webhook:', error);
|
||||||
|
return json({ error: 'Failed to create test webhook' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
85
sveltekit-app/src/routes/api/ws/+server.ts
Normal file
85
sveltekit-app/src/routes/api/ws/+server.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { prisma } from '$lib/db';
|
||||||
|
import { clients } from '../../webhook/[...path]/+server.js';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ request, locals }) => {
|
||||||
|
const session = await locals.getSession();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return new Response('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade to WebSocket
|
||||||
|
const upgrade = request.headers.get('upgrade');
|
||||||
|
if (upgrade !== 'websocket') {
|
||||||
|
return new Response('Expected websocket', { status: 426 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { socket, response } = Deno.upgradeWebSocket(request);
|
||||||
|
|
||||||
|
// Add client to the map
|
||||||
|
if (!clients.has(user.subdomain)) {
|
||||||
|
clients.set(user.subdomain, []);
|
||||||
|
}
|
||||||
|
clients.get(user.subdomain)!.push(socket);
|
||||||
|
|
||||||
|
// Send welcome message
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
message: `Connected to WebSocket server as ${user.name} with subdomain ${user.subdomain}`,
|
||||||
|
type: 'connection'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Handle WebSocket events
|
||||||
|
socket.onopen = () => {
|
||||||
|
console.log(`WebSocket connected for user: ${user.subdomain}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('Received message:', data);
|
||||||
|
|
||||||
|
// Echo back for testing
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
message: 'Message received',
|
||||||
|
type: 'echo',
|
||||||
|
data: data
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
console.log(`WebSocket disconnected for user: ${user.subdomain}`);
|
||||||
|
|
||||||
|
// Remove client from the map
|
||||||
|
const userClients = clients.get(user.subdomain);
|
||||||
|
if (userClients) {
|
||||||
|
const index = userClients.indexOf(socket);
|
||||||
|
if (index > -1) {
|
||||||
|
userClients.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty arrays
|
||||||
|
if (userClients.length === 0) {
|
||||||
|
clients.delete(user.subdomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
console.error(`WebSocket error for user ${user.subdomain}:`, error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
152
sveltekit-app/src/routes/webhook/[...path]/+server.ts
Normal file
152
sveltekit-app/src/routes/webhook/[...path]/+server.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { prisma } from '$lib/db';
|
||||||
|
import { relayWebhookToTargets, storeRelayResults } from '$lib/relay';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
// Store connected WebSocket clients for real-time updates
|
||||||
|
const clients: Map<string, any[]> = new Map();
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
return handleWebhook(request, params, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
return handleWebhook(request, params, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUT: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
return handleWebhook(request, params, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
return handleWebhook(request, params, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PATCH: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
return handleWebhook(request, params, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleWebhook(request: Request, params: any, url: URL) {
|
||||||
|
try {
|
||||||
|
// Extract subdomain from hostname
|
||||||
|
const hostname = request.headers.get('host') || '';
|
||||||
|
const urlParts = hostname.split('.');
|
||||||
|
let subdomain = '';
|
||||||
|
|
||||||
|
if (urlParts.length > 1) {
|
||||||
|
subdomain = urlParts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subdomain) {
|
||||||
|
return json({ error: 'Missing Subdomain' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user by subdomain
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { subdomain },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ error: 'Invalid Subdomain' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get request body
|
||||||
|
let body: any = null;
|
||||||
|
const contentType = request.headers.get('content-type');
|
||||||
|
|
||||||
|
if (contentType?.includes('application/json')) {
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
body = null;
|
||||||
|
}
|
||||||
|
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
body = Object.fromEntries(formData);
|
||||||
|
} else {
|
||||||
|
// Try to get raw body as text
|
||||||
|
try {
|
||||||
|
body = await request.text();
|
||||||
|
} catch {
|
||||||
|
body = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get headers (excluding sensitive ones)
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (const [key, value] of request.headers.entries()) {
|
||||||
|
if (!['authorization', 'cookie', 'x-forwarded-for'].includes(key.toLowerCase())) {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build webhook event
|
||||||
|
const webhookEvent = {
|
||||||
|
userId: user.id,
|
||||||
|
method: request.method,
|
||||||
|
path: url.pathname,
|
||||||
|
query: url.search,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: JSON.stringify(headers),
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store in database
|
||||||
|
const storedEvent = await prisma.webhookEvent.create({
|
||||||
|
data: webhookEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relay to configured targets (async, don't wait for completion)
|
||||||
|
const relayResults = await relayWebhookToTargets(user.id, {
|
||||||
|
method: webhookData.method,
|
||||||
|
path: webhookData.path,
|
||||||
|
query: webhookData.query,
|
||||||
|
body: body,
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store relay results for analytics
|
||||||
|
storeRelayResults(storedEvent.id, relayResults);
|
||||||
|
|
||||||
|
// Broadcast to WebSocket clients
|
||||||
|
let messageSent = false;
|
||||||
|
if (clients.has(subdomain)) {
|
||||||
|
try {
|
||||||
|
const userClients = clients.get(subdomain) || [];
|
||||||
|
userClients.forEach((client) => {
|
||||||
|
if (client.readyState === 1) { // WebSocket.OPEN
|
||||||
|
client.send(JSON.stringify({
|
||||||
|
...storedEvent,
|
||||||
|
user: {
|
||||||
|
name: user.name,
|
||||||
|
subdomain: user.subdomain
|
||||||
|
},
|
||||||
|
relayResults: relayResults
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
messageSent = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error broadcasting to WebSocket clients:', error);
|
||||||
|
messageSent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
logged: true,
|
||||||
|
forwarded: messageSent,
|
||||||
|
subdomain,
|
||||||
|
eventId: storedEvent.id,
|
||||||
|
timestamp: storedEvent.createdAt
|
||||||
|
}, { status: 200 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling webhook:', error);
|
||||||
|
return json({ error: 'Internal Server Error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the clients map for WebSocket handler
|
||||||
|
export { clients };
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
wsClient = createWebSocketClient(`ws://localhost:4200/api/relay`);
|
wsClient = createWebSocketClient();
|
||||||
|
|
||||||
wsClient.events.subscribe((newEvents) => {
|
wsClient.events.subscribe((newEvents) => {
|
||||||
// Merge new events with existing ones
|
// Merge new events with existing ones
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-node';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
@@ -11,7 +11,12 @@ const config = {
|
|||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
|
|
||||||
|
// Enable WebSocket support
|
||||||
|
alias: {
|
||||||
|
$lib: './src/lib'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user