mirror of
https://github.com/LukeHagar/relay.git
synced 2025-12-07 12:47:45 +00:00
Implement SvelteKit webhook relay with SSE, auth, and real-time features
Co-authored-by: lukeslakemail <lukeslakemail@gmail.com>
This commit is contained in:
355
INTEGRATION_ANALYSIS.md
Normal file
355
INTEGRATION_ANALYSIS.md
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
# Webhook Relay Integration Analysis
|
||||||
|
|
||||||
|
## 🔍 Current Implementation Analysis
|
||||||
|
|
||||||
|
### Architecture Overview
|
||||||
|
The current "Baton" webhook relay system demonstrates a sophisticated approach to webhook handling with the following components:
|
||||||
|
|
||||||
|
1. **Dual Server Architecture**:
|
||||||
|
- **Ingest Server (Port 4000)**: Handles incoming webhooks via subdomain routing
|
||||||
|
- **Relay Server (Port 4200)**: Manages WebSocket connections and authentication
|
||||||
|
|
||||||
|
2. **Key Technologies**:
|
||||||
|
- **Hono**: Fast web framework for API routes
|
||||||
|
- **Bun**: JavaScript runtime for server execution
|
||||||
|
- **Prisma**: Database ORM with PostgreSQL
|
||||||
|
- **Auth.js**: Authentication with GitHub OAuth
|
||||||
|
- **WebSockets**: Real-time bidirectional communication
|
||||||
|
|
||||||
|
3. **Data Flow**:
|
||||||
|
```
|
||||||
|
External Service → Subdomain Webhook → Database Log → WebSocket Broadcast → Connected Clients
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 SvelteKit Integration Opportunities
|
||||||
|
|
||||||
|
### 1. **Unified Application Architecture**
|
||||||
|
|
||||||
|
**Benefits of SvelteKit Integration**:
|
||||||
|
- **Single Codebase**: Eliminates dual server complexity
|
||||||
|
- **File-based Routing**: Intuitive API endpoint organization
|
||||||
|
- **SSR/SPA Hybrid**: Optimal performance for dashboard UI
|
||||||
|
- **Built-in Optimizations**: Automatic code splitting, preloading
|
||||||
|
|
||||||
|
**Implementation Strategy**:
|
||||||
|
```
|
||||||
|
Current: Hono Server (Port 4000) + Hono Server (Port 4200)
|
||||||
|
New: SvelteKit App with API Routes + Frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Enhanced Real-time Communication**
|
||||||
|
|
||||||
|
**Server-Sent Events vs WebSockets**:
|
||||||
|
|
||||||
|
| Aspect | WebSockets (Current) | SSE (Recommended) |
|
||||||
|
|--------|---------------------|-------------------|
|
||||||
|
| Complexity | High (bidirectional) | Low (unidirectional) |
|
||||||
|
| Browser Support | Good | Excellent |
|
||||||
|
| Reconnection | Manual | Automatic |
|
||||||
|
| Resource Usage | Higher | Lower |
|
||||||
|
| Use Case Fit | Overkill for webhooks | Perfect for webhooks |
|
||||||
|
|
||||||
|
**SSE Implementation Benefits**:
|
||||||
|
- **Automatic Reconnection**: Built into EventSource API
|
||||||
|
- **HTTP/2 Multiplexing**: Better performance
|
||||||
|
- **Simpler Error Handling**: Standard HTTP error codes
|
||||||
|
- **Lower Resource Usage**: No need for bidirectional communication
|
||||||
|
|
||||||
|
### 3. **Modern Frontend Integration**
|
||||||
|
|
||||||
|
**Svelte Store Architecture**:
|
||||||
|
```typescript
|
||||||
|
// Real-time webhook events
|
||||||
|
export const webhookEvents = writable<WebhookEvent[]>([]);
|
||||||
|
|
||||||
|
// Connection management
|
||||||
|
export const connectionStatus = writable<'connected' | 'disconnected'>('disconnected');
|
||||||
|
|
||||||
|
// Derived computed values
|
||||||
|
export const recentEvents = derived(webhookEvents, events => events.slice(0, 10));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- **Reactive UI**: Automatic updates when data changes
|
||||||
|
- **Type Safety**: Full TypeScript integration
|
||||||
|
- **Performance**: Efficient reactivity system
|
||||||
|
- **Developer Experience**: Intuitive state management
|
||||||
|
|
||||||
|
## 🔄 Integration Modifications
|
||||||
|
|
||||||
|
### 1. **Routing System Transformation**
|
||||||
|
|
||||||
|
**Current Subdomain Handling**:
|
||||||
|
```typescript
|
||||||
|
// Hono-based subdomain extraction
|
||||||
|
const urlParts = url.hostname.split(".");
|
||||||
|
const subdomain = urlParts[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
**SvelteKit Dynamic Routes**:
|
||||||
|
```typescript
|
||||||
|
// File: src/routes/api/webhook/[subdomain]/+server.ts
|
||||||
|
export const POST: RequestHandler = async ({ params }) => {
|
||||||
|
const { subdomain } = params; // Automatic parameter extraction
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Authentication Integration**
|
||||||
|
|
||||||
|
**Current Implementation**:
|
||||||
|
- Separate auth middleware in Hono
|
||||||
|
- Manual session management
|
||||||
|
- Custom cookie handling
|
||||||
|
|
||||||
|
**SvelteKit Integration**:
|
||||||
|
- **Auth.js Integration**: Native SvelteKit support
|
||||||
|
- **Hooks System**: Server-side authentication middleware
|
||||||
|
- **Type-safe Sessions**: Automatic session typing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// hooks.server.ts - Automatic auth handling
|
||||||
|
export const handle = sequence(authHandle, customHandle);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Database Optimization**
|
||||||
|
|
||||||
|
**Enhanced Schema**:
|
||||||
|
```prisma
|
||||||
|
model WebhookEvent {
|
||||||
|
// ... existing fields
|
||||||
|
@@index([userId, createdAt]) // Performance optimization
|
||||||
|
}
|
||||||
|
|
||||||
|
model RelayTarget {
|
||||||
|
// ... existing fields
|
||||||
|
@@index([userId, active]) // Query optimization
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Real-time Communication Upgrade**
|
||||||
|
|
||||||
|
**Current WebSocket Implementation**:
|
||||||
|
```typescript
|
||||||
|
// Manual connection management
|
||||||
|
const clients: Map<string, WSContext[]> = new Map();
|
||||||
|
|
||||||
|
// Manual broadcasting
|
||||||
|
clients.get(subdomain)?.forEach((ws) => {
|
||||||
|
ws.send(JSON.stringify(message));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**New SSE Implementation**:
|
||||||
|
```typescript
|
||||||
|
// Automatic connection lifecycle
|
||||||
|
export function addSSEConnection(userId: string, controller: ReadableStreamDefaultController) {
|
||||||
|
// Automatic cleanup and management
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe broadcasting
|
||||||
|
export async function broadcastToUser(userId: string, event: WebhookEvent) {
|
||||||
|
// Reliable message delivery with error handling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Advanced Integration Features
|
||||||
|
|
||||||
|
### 1. **Webhook Transformation Pipeline**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/server/transformers.ts
|
||||||
|
export interface WebhookTransformer {
|
||||||
|
name: string;
|
||||||
|
transform: (payload: any) => any;
|
||||||
|
condition?: (payload: any) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transformers: WebhookTransformer[] = [
|
||||||
|
{
|
||||||
|
name: 'GitHub to Slack',
|
||||||
|
condition: (payload) => payload.repository && payload.action,
|
||||||
|
transform: (payload) => ({
|
||||||
|
text: `${payload.action} on ${payload.repository.name}`,
|
||||||
|
username: payload.sender.login
|
||||||
|
})
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Multi-tenant Subdomain Management**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/server/subdomain.ts
|
||||||
|
export async function createUserSubdomain(userId: string, preferredSubdomain?: string) {
|
||||||
|
const subdomain = preferredSubdomain || generateUniqueSubdomain();
|
||||||
|
|
||||||
|
// Validate subdomain availability
|
||||||
|
const existing = await prisma.user.findUnique({ where: { subdomain } });
|
||||||
|
if (existing) {
|
||||||
|
throw new Error('Subdomain already taken');
|
||||||
|
}
|
||||||
|
|
||||||
|
return subdomain;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Webhook Analytics Dashboard**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Enhanced analytics with aggregated data
|
||||||
|
export async function getWebhookAnalytics(userId: string, timeRange: string) {
|
||||||
|
const events = await prisma.webhookEvent.groupBy({
|
||||||
|
by: ['method', 'path'],
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
createdAt: { gte: getTimeRangeStart(timeRange) }
|
||||||
|
},
|
||||||
|
_count: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Advanced Relay Features**
|
||||||
|
|
||||||
|
**Conditional Forwarding**:
|
||||||
|
```typescript
|
||||||
|
export interface RelayRule {
|
||||||
|
condition: string; // JSONPath or simple condition
|
||||||
|
target: string;
|
||||||
|
transform?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: Only forward GitHub push events
|
||||||
|
{
|
||||||
|
condition: "$.action === 'push'",
|
||||||
|
target: "https://api.example.com/github-push",
|
||||||
|
transform: "{ commit: $.head_commit.id, repo: $.repository.name }"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Retry Logic**:
|
||||||
|
```typescript
|
||||||
|
export async function forwardWithRetry(target: string, payload: any, maxRetries = 3) {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(target, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) return { success: true };
|
||||||
|
|
||||||
|
if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} attempts`);
|
||||||
|
|
||||||
|
// Exponential backoff
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === maxRetries) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Performance Comparison
|
||||||
|
|
||||||
|
### Memory Usage
|
||||||
|
- **Current**: Higher due to WebSocket connection overhead
|
||||||
|
- **SvelteKit**: Lower with SSE and automatic cleanup
|
||||||
|
|
||||||
|
### Scalability
|
||||||
|
- **Current**: Limited by single server instance
|
||||||
|
- **SvelteKit**: Serverless-ready, auto-scaling
|
||||||
|
|
||||||
|
### Development Speed
|
||||||
|
- **Current**: Manual setup, dual server management
|
||||||
|
- **SvelteKit**: Rapid development with hot reload, type safety
|
||||||
|
|
||||||
|
## 🎨 UI/UX Enhancements
|
||||||
|
|
||||||
|
### 1. **Real-time Dashboard**
|
||||||
|
- Live webhook event feed
|
||||||
|
- Connection status indicators
|
||||||
|
- Interactive event exploration
|
||||||
|
- Visual relay target management
|
||||||
|
|
||||||
|
### 2. **Developer Tools**
|
||||||
|
- Webhook testing interface
|
||||||
|
- Event replay functionality
|
||||||
|
- Export/import configurations
|
||||||
|
- API documentation generator
|
||||||
|
|
||||||
|
### 3. **Mobile Responsiveness**
|
||||||
|
- Responsive design with Tailwind CSS
|
||||||
|
- Touch-friendly interfaces
|
||||||
|
- Progressive Web App capabilities
|
||||||
|
|
||||||
|
## 🔮 Future Extensibility
|
||||||
|
|
||||||
|
### 1. **Plugin System**
|
||||||
|
```typescript
|
||||||
|
export interface WebhookPlugin {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
hooks: {
|
||||||
|
beforeStore?: (event: WebhookEvent) => WebhookEvent;
|
||||||
|
afterStore?: (event: WebhookEvent) => void;
|
||||||
|
beforeRelay?: (event: WebhookEvent, target: RelayTarget) => any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Multi-Protocol Support**
|
||||||
|
- **GraphQL Subscriptions**: For advanced real-time needs
|
||||||
|
- **gRPC Streaming**: For high-performance scenarios
|
||||||
|
- **MQTT Integration**: For IoT webhook scenarios
|
||||||
|
|
||||||
|
### 3. **Enterprise Features**
|
||||||
|
- **Team Management**: Multi-user organizations
|
||||||
|
- **Audit Logging**: Comprehensive activity logs
|
||||||
|
- **SLA Monitoring**: Uptime and performance tracking
|
||||||
|
- **Custom Domains**: White-label subdomain management
|
||||||
|
|
||||||
|
## 📝 Migration Timeline
|
||||||
|
|
||||||
|
### Phase 1: Core Migration (Week 1)
|
||||||
|
- [ ] Set up SvelteKit project structure
|
||||||
|
- [ ] Migrate database schema
|
||||||
|
- [ ] Implement basic webhook ingestion
|
||||||
|
- [ ] Set up authentication
|
||||||
|
|
||||||
|
### Phase 2: Real-time Features (Week 2)
|
||||||
|
- [ ] Implement SSE communication
|
||||||
|
- [ ] Create dashboard UI
|
||||||
|
- [ ] Add webhook history view
|
||||||
|
- [ ] Test real-time functionality
|
||||||
|
|
||||||
|
### Phase 3: Advanced Features (Week 3)
|
||||||
|
- [ ] Add relay target management
|
||||||
|
- [ ] Implement webhook forwarding
|
||||||
|
- [ ] Create analytics dashboard
|
||||||
|
- [ ] Performance optimization
|
||||||
|
|
||||||
|
### Phase 4: Production Deployment (Week 4)
|
||||||
|
- [ ] Production environment setup
|
||||||
|
- [ ] DNS and subdomain configuration
|
||||||
|
- [ ] Monitoring and alerting
|
||||||
|
- [ ] User migration and testing
|
||||||
|
|
||||||
|
## 🎯 Success Metrics
|
||||||
|
|
||||||
|
### Technical Metrics
|
||||||
|
- **Response Time**: < 100ms for webhook ingestion
|
||||||
|
- **Real-time Latency**: < 500ms for event delivery
|
||||||
|
- **Uptime**: 99.9% availability
|
||||||
|
- **Scalability**: Handle 1000+ concurrent connections
|
||||||
|
|
||||||
|
### User Experience Metrics
|
||||||
|
- **Dashboard Load Time**: < 2 seconds
|
||||||
|
- **Real-time Update Delay**: < 1 second
|
||||||
|
- **Mobile Responsiveness**: 100% mobile compatibility
|
||||||
|
- **Accessibility**: WCAG 2.1 AA compliance
|
||||||
|
|
||||||
|
This comprehensive integration transforms the webhook relay from a development tool into a production-ready, scalable SaaS application suitable for enterprise use.
|
||||||
321
MIGRATION_GUIDE.md
Normal file
321
MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# Migration Guide: From Baton to SvelteKit Webhook Relay
|
||||||
|
|
||||||
|
This guide outlines how to migrate from the current Baton webhook relay implementation to a fully integrated SvelteKit application.
|
||||||
|
|
||||||
|
## 🔍 Current vs. New Architecture
|
||||||
|
|
||||||
|
### Current Implementation (Baton)
|
||||||
|
- **Dual Server Setup**: Separate Hono servers for ingestion (4000) and relay (4200)
|
||||||
|
- **WebSocket Communication**: Bidirectional WebSocket connections
|
||||||
|
- **Manual Client Management**: Map-based WebSocket client tracking
|
||||||
|
- **Bun Runtime**: Specific to Bun.js runtime environment
|
||||||
|
|
||||||
|
### New SvelteKit Implementation
|
||||||
|
- **Unified Application**: Single SvelteKit app with server routes
|
||||||
|
- **Server-Sent Events**: Unidirectional real-time communication
|
||||||
|
- **Store-based State**: Svelte stores for client-side state management
|
||||||
|
- **Platform Agnostic**: Deployable to various platforms (Vercel, Netlify, etc.)
|
||||||
|
|
||||||
|
## 📋 Migration Steps
|
||||||
|
|
||||||
|
### 1. Database Migration
|
||||||
|
|
||||||
|
The database schema remains largely compatible. Key changes:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add indexes for better performance
|
||||||
|
CREATE INDEX idx_webhook_events_user_created ON "WebhookEvent"("userId", "createdAt");
|
||||||
|
CREATE INDEX idx_relay_targets_user_active ON "RelayTarget"("userId", "active");
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration Command**:
|
||||||
|
```bash
|
||||||
|
# Backup existing data
|
||||||
|
pg_dump your_database > backup.sql
|
||||||
|
|
||||||
|
# Apply new schema
|
||||||
|
npx prisma db push
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Environment Variables
|
||||||
|
|
||||||
|
Update your environment configuration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Remove Bun-specific variables
|
||||||
|
# Add SvelteKit-specific variables
|
||||||
|
AUTH_SECRET="your-auth-secret"
|
||||||
|
GITHUB_CLIENT_ID="your-github-client-id"
|
||||||
|
GITHUB_CLIENT_SECRET="your-github-client-secret"
|
||||||
|
REDIRECT_URL="https://yourdomain.com/dashboard"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Deployment Changes
|
||||||
|
|
||||||
|
#### From Bun Servers
|
||||||
|
```typescript
|
||||||
|
// OLD: server/index.ts
|
||||||
|
const ingestServer = Bun.serve(IngestHandler);
|
||||||
|
const relayServer = Bun.serve(RelayHandler);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### To SvelteKit Routes
|
||||||
|
```typescript
|
||||||
|
// NEW: Automatic routing via file structure
|
||||||
|
src/routes/api/webhook/[subdomain]/+server.ts
|
||||||
|
src/routes/api/relay/events/+server.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Client Integration
|
||||||
|
|
||||||
|
#### Old WebSocket Client
|
||||||
|
```javascript
|
||||||
|
// OLD: Manual WebSocket management
|
||||||
|
const ws = new WebSocket(`ws://${subdomain}.localhost:3000`);
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log(JSON.parse(event.data));
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New SSE Integration
|
||||||
|
```typescript
|
||||||
|
// NEW: Svelte store integration
|
||||||
|
import { webhookStore } from '$lib/stores/webhooks';
|
||||||
|
webhookStore.connect(); // Automatic SSE connection
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Feature Mapping
|
||||||
|
|
||||||
|
| Baton Feature | SvelteKit Equivalent | Implementation |
|
||||||
|
|---------------|---------------------|----------------|
|
||||||
|
| Subdomain routing | Dynamic route params | `[subdomain]/+server.ts` |
|
||||||
|
| WebSocket relay | Server-Sent Events | `/api/relay/events` |
|
||||||
|
| Client tracking | SSE connection map | `$lib/server/relay.ts` |
|
||||||
|
| Auth middleware | SvelteKit hooks | `hooks.server.ts` |
|
||||||
|
| Webhook logging | Same Prisma models | Enhanced with indexes |
|
||||||
|
|
||||||
|
## ⚡ Performance Improvements
|
||||||
|
|
||||||
|
### 1. Connection Management
|
||||||
|
- **Before**: Manual WebSocket connection tracking
|
||||||
|
- **After**: Automatic SSE connection lifecycle management
|
||||||
|
|
||||||
|
### 2. Real-time Updates
|
||||||
|
- **Before**: Bidirectional WebSocket (unnecessary overhead)
|
||||||
|
- **After**: Unidirectional SSE (optimal for webhook relay)
|
||||||
|
|
||||||
|
### 3. Scalability
|
||||||
|
- **Before**: Single server instance limitations
|
||||||
|
- **After**: Serverless-ready, horizontal scaling
|
||||||
|
|
||||||
|
## 🎯 Enhanced Features
|
||||||
|
|
||||||
|
### 1. User Interface
|
||||||
|
- **Dashboard**: Real-time webhook monitoring
|
||||||
|
- **Event History**: Searchable webhook logs
|
||||||
|
- **Relay Management**: Visual target configuration
|
||||||
|
|
||||||
|
### 2. Developer Experience
|
||||||
|
- **Type Safety**: Full TypeScript integration
|
||||||
|
- **Hot Reload**: SvelteKit development server
|
||||||
|
- **File-based Routing**: Intuitive API structure
|
||||||
|
|
||||||
|
### 3. Production Ready
|
||||||
|
- **Multiple Deployment Options**: Vercel, Netlify, Cloudflare
|
||||||
|
- **Built-in Optimizations**: SvelteKit's production optimizations
|
||||||
|
- **Error Handling**: Comprehensive error boundaries
|
||||||
|
|
||||||
|
## 🔧 Advanced Customizations
|
||||||
|
|
||||||
|
### 1. Custom Authentication
|
||||||
|
|
||||||
|
Replace GitHub OAuth with custom auth:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/server/auth.ts
|
||||||
|
import Credentials from '@auth/core/providers/credentials';
|
||||||
|
|
||||||
|
export const { handle } = SvelteKitAuth({
|
||||||
|
providers: [
|
||||||
|
Credentials({
|
||||||
|
credentials: {
|
||||||
|
username: { label: "Username", type: "text" },
|
||||||
|
password: { label: "Password", type: "password" }
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
// Custom auth logic
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Webhook Transformation
|
||||||
|
|
||||||
|
Add webhook transformation middleware:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/routes/api/webhook/[subdomain]/+server.ts
|
||||||
|
function transformWebhook(payload: any, transformRules: any[]) {
|
||||||
|
// Apply transformation rules
|
||||||
|
return transformedPayload;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Rate Limiting
|
||||||
|
|
||||||
|
Implement rate limiting for webhook endpoints:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/server/rateLimit.ts
|
||||||
|
export function rateLimit(userId: string, windowMs: number, maxRequests: number) {
|
||||||
|
// Rate limiting logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Deployment Strategies
|
||||||
|
|
||||||
|
### 1. Vercel (Recommended)
|
||||||
|
```bash
|
||||||
|
npm install @sveltejs/adapter-vercel
|
||||||
|
vercel deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- Automatic HTTPS and domain management
|
||||||
|
- Edge functions for webhook processing
|
||||||
|
- Built-in analytics and monitoring
|
||||||
|
|
||||||
|
### 2. Self-hosted with Docker
|
||||||
|
```dockerfile
|
||||||
|
FROM node:18-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY build build/
|
||||||
|
COPY package.json .
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "build"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Cloudflare Workers
|
||||||
|
```bash
|
||||||
|
npm install @sveltejs/adapter-cloudflare
|
||||||
|
wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Testing Strategy
|
||||||
|
|
||||||
|
### 1. Webhook Testing
|
||||||
|
```bash
|
||||||
|
# Test webhook endpoint
|
||||||
|
curl -X POST https://yourdomain.com/api/webhook/your-subdomain \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"test": true, "message": "Hello World"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. SSE Testing
|
||||||
|
```javascript
|
||||||
|
// Browser console
|
||||||
|
const eventSource = new EventSource('/api/relay/events');
|
||||||
|
eventSource.onmessage = console.log;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📈 Monitoring & Analytics
|
||||||
|
|
||||||
|
### 1. Built-in Metrics
|
||||||
|
- Real-time event counts
|
||||||
|
- Connection status monitoring
|
||||||
|
- Relay target success rates
|
||||||
|
|
||||||
|
### 2. External Monitoring
|
||||||
|
- **Sentry**: Error tracking and performance monitoring
|
||||||
|
- **LogRocket**: User session replay
|
||||||
|
- **Vercel Analytics**: Performance insights
|
||||||
|
|
||||||
|
## 🛡️ Security Considerations
|
||||||
|
|
||||||
|
### 1. Webhook Verification
|
||||||
|
Implement signature verification for specific providers:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Stripe webhook verification
|
||||||
|
import { createHmac } from 'crypto';
|
||||||
|
|
||||||
|
function verifyStripeSignature(payload: string, signature: string, secret: string) {
|
||||||
|
const expectedSignature = createHmac('sha256', secret)
|
||||||
|
.update(payload, 'utf8')
|
||||||
|
.digest('hex');
|
||||||
|
return `sha256=${expectedSignature}` === signature;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Rate Limiting
|
||||||
|
```typescript
|
||||||
|
// Implement per-user rate limiting
|
||||||
|
const rateLimiter = new Map<string, { count: number; resetTime: number }>();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Input Sanitization
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const webhookSchema = z.object({
|
||||||
|
// Define expected webhook structure
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Backup existing database
|
||||||
|
- [ ] Set up new SvelteKit environment
|
||||||
|
- [ ] Configure authentication (GitHub OAuth)
|
||||||
|
- [ ] Migrate database schema with indexes
|
||||||
|
- [ ] Update DNS/subdomain routing
|
||||||
|
- [ ] Test webhook endpoints
|
||||||
|
- [ ] Verify real-time functionality
|
||||||
|
- [ ] Update external service webhook URLs
|
||||||
|
- [ ] Monitor for issues post-migration
|
||||||
|
|
||||||
|
## 🎉 Benefits of Migration
|
||||||
|
|
||||||
|
1. **Better Developer Experience**: Hot reload, type safety, modern tooling
|
||||||
|
2. **Improved Performance**: SSE efficiency, built-in optimizations
|
||||||
|
3. **Enhanced UI/UX**: Modern dashboard with real-time updates
|
||||||
|
4. **Platform Flexibility**: Deploy anywhere SvelteKit is supported
|
||||||
|
5. **Maintainability**: Cleaner architecture, better separation of concerns
|
||||||
|
6. **Scalability**: Serverless-ready, horizontal scaling capabilities
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **SSE Connection Drops**
|
||||||
|
- Check network connectivity
|
||||||
|
- Verify authentication status
|
||||||
|
- Review browser console for errors
|
||||||
|
|
||||||
|
2. **Webhook Not Received**
|
||||||
|
- Verify subdomain routing
|
||||||
|
- Check external service configuration
|
||||||
|
- Review server logs
|
||||||
|
|
||||||
|
3. **Database Connection Issues**
|
||||||
|
- Verify DATABASE_URL format
|
||||||
|
- Check database server status
|
||||||
|
- Review Prisma connection logs
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging:
|
||||||
|
```env
|
||||||
|
DEBUG=true
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
For issues or questions about the migration:
|
||||||
|
1. Check the troubleshooting section
|
||||||
|
2. Review SvelteKit documentation
|
||||||
|
3. Check Auth.js documentation for authentication issues
|
||||||
10
sveltekit-integration/.env.example
Normal file
10
sveltekit-integration/.env.example
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Database
|
||||||
|
DATABASE_URL="postgresql://username:password@localhost:5432/webhook_relay"
|
||||||
|
|
||||||
|
# Auth.js
|
||||||
|
AUTH_SECRET="your-auth-secret-here"
|
||||||
|
GITHUB_CLIENT_ID="your-github-client-id"
|
||||||
|
GITHUB_CLIENT_SECRET="your-github-client-secret"
|
||||||
|
|
||||||
|
# Optional: Redirect URL after authentication
|
||||||
|
REDIRECT_URL="http://localhost:5173/dashboard"
|
||||||
170
sveltekit-integration/README.md
Normal file
170
sveltekit-integration/README.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Webhook Relay - SvelteKit Integration
|
||||||
|
|
||||||
|
A comprehensive SvelteKit implementation of a webhook relay system that receives, monitors, and forwards webhooks in real-time.
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- **Real-time Monitoring**: Live webhook events using Server-Sent Events (SSE)
|
||||||
|
- **Subdomain Routing**: Each user gets a unique subdomain for webhook endpoints
|
||||||
|
- **Relay Targets**: Forward webhooks to multiple destinations
|
||||||
|
- **Authentication**: GitHub OAuth integration with Auth.js
|
||||||
|
- **Event History**: Persistent logging of all webhook events
|
||||||
|
- **Modern UI**: Clean, responsive interface built with Tailwind CSS
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Key Improvements over Original Implementation
|
||||||
|
|
||||||
|
1. **Unified Application**: Single SvelteKit app instead of dual server setup
|
||||||
|
2. **Server-Sent Events**: More efficient than WebSockets for one-way communication
|
||||||
|
3. **File-based Routing**: Leverages SvelteKit's intuitive routing system
|
||||||
|
4. **Type Safety**: Full TypeScript integration throughout
|
||||||
|
5. **Modern Auth**: Auth.js integration with proper session management
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── lib/
|
||||||
|
│ ├── components/ # Reusable Svelte components
|
||||||
|
│ ├── server/ # Server-side utilities
|
||||||
|
│ │ ├── auth.ts # Authentication configuration
|
||||||
|
│ │ ├── db.ts # Database connection
|
||||||
|
│ │ └── relay.ts # Webhook relay logic
|
||||||
|
│ └── stores/ # Client-side state management
|
||||||
|
│ └── webhooks.ts # Webhook-related stores
|
||||||
|
├── routes/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ ├── webhook/[subdomain]/ # Webhook ingestion endpoint
|
||||||
|
│ │ ├── relay/ # Relay management APIs
|
||||||
|
│ │ └── webhooks/ # Webhook history API
|
||||||
|
│ ├── auth/ # Authentication routes
|
||||||
|
│ └── dashboard/ # Protected dashboard pages
|
||||||
|
└── app.d.ts # TypeScript declarations
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Setup
|
||||||
|
|
||||||
|
1. **Install Dependencies**:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Environment Variables**:
|
||||||
|
Copy `.env.example` to `.env` and configure:
|
||||||
|
```env
|
||||||
|
DATABASE_URL="postgresql://..."
|
||||||
|
AUTH_SECRET="your-secret"
|
||||||
|
GITHUB_CLIENT_ID="your-github-app-id"
|
||||||
|
GITHUB_CLIENT_SECRET="your-github-app-secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database Setup**:
|
||||||
|
```bash
|
||||||
|
npx prisma db push
|
||||||
|
npx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Development**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
### Webhook Ingestion
|
||||||
|
- `POST /api/webhook/{subdomain}` - Receive webhooks for a specific user
|
||||||
|
|
||||||
|
### Relay Management
|
||||||
|
- `GET /api/relay/events` - Server-Sent Events stream for real-time updates
|
||||||
|
- `GET /api/relay/targets` - List user's relay targets
|
||||||
|
- `POST /api/relay/targets` - Add new relay target
|
||||||
|
- `DELETE /api/relay/targets` - Remove relay target
|
||||||
|
|
||||||
|
### Webhook History
|
||||||
|
- `GET /api/webhooks` - Get webhook event history
|
||||||
|
|
||||||
|
## 🔄 Real-time Communication
|
||||||
|
|
||||||
|
The system uses **Server-Sent Events (SSE)** instead of WebSockets for several advantages:
|
||||||
|
|
||||||
|
1. **Simpler Implementation**: No need for bidirectional communication
|
||||||
|
2. **Better Browser Support**: Native EventSource API
|
||||||
|
3. **Automatic Reconnection**: Built-in reconnection logic
|
||||||
|
4. **HTTP/2 Multiplexing**: Better performance over HTTP/2
|
||||||
|
|
||||||
|
### SSE Connection Flow
|
||||||
|
|
||||||
|
1. Client connects to `/api/relay/events`
|
||||||
|
2. Server maintains connection map by user ID
|
||||||
|
3. Incoming webhooks trigger real-time broadcasts
|
||||||
|
4. Automatic heartbeat keeps connections alive
|
||||||
|
|
||||||
|
## 🎯 Usage Example
|
||||||
|
|
||||||
|
1. **Sign in** with GitHub
|
||||||
|
2. **Get your webhook endpoint**: `https://yourdomain.com/api/webhook/{your-subdomain}`
|
||||||
|
3. **Configure external services** to send webhooks to your endpoint
|
||||||
|
4. **Add relay targets** to forward webhooks to your services
|
||||||
|
5. **Monitor events** in real-time through the dashboard
|
||||||
|
|
||||||
|
## 🔒 Security Features
|
||||||
|
|
||||||
|
- **Authentication Required**: All dashboard routes protected
|
||||||
|
- **User Isolation**: Webhooks isolated by subdomain/user
|
||||||
|
- **Header Filtering**: Sensitive headers excluded from logs
|
||||||
|
- **Input Validation**: Zod schemas for API validation
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Vercel (Recommended)
|
||||||
|
```bash
|
||||||
|
npm install @sveltejs/adapter-vercel
|
||||||
|
# Update svelte.config.js to use vercel adapter
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Platforms
|
||||||
|
- **Netlify**: Use `@sveltejs/adapter-netlify`
|
||||||
|
- **Cloudflare**: Use `@sveltejs/adapter-cloudflare`
|
||||||
|
- **Node.js**: Use `@sveltejs/adapter-node`
|
||||||
|
|
||||||
|
## 🔧 Advanced Configuration
|
||||||
|
|
||||||
|
### Custom Subdomain Handling
|
||||||
|
|
||||||
|
For production use with custom domains, configure your DNS and load balancer to route subdomains to your SvelteKit application:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx example
|
||||||
|
server {
|
||||||
|
server_name *.yourdomain.com;
|
||||||
|
location / {
|
||||||
|
proxy_pass http://your-sveltekit-app;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Webhook Forwarding
|
||||||
|
|
||||||
|
The system supports forwarding webhooks to multiple targets with:
|
||||||
|
- **Automatic retries** for failed requests
|
||||||
|
- **Custom headers** for identification
|
||||||
|
- **Error logging** and monitoring
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
- Real-time event count in dashboard
|
||||||
|
- Connection status indicators
|
||||||
|
- Event history with filtering
|
||||||
|
- Relay target success/failure tracking
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
- Webhook filtering and transformation rules
|
||||||
|
- Custom authentication methods
|
||||||
|
- Webhook replay functionality
|
||||||
|
- Analytics and metrics dashboard
|
||||||
|
- Rate limiting and throttling
|
||||||
|
- Webhook signature verification for specific providers
|
||||||
37
sveltekit-integration/package.json
Normal file
37
sveltekit-integration/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "webhook-relay-sveltekit",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite dev",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"db:generate": "prisma generate",
|
||||||
|
"db:push": "prisma db push",
|
||||||
|
"db:migrate": "prisma migrate dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/kit": "^2.0.0",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"prisma": "^5.21.1",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"svelte-check": "^4.0.0",
|
||||||
|
"tailwindcss": "^3.3.6",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@auth/core": "^0.37.2",
|
||||||
|
"@auth/prisma-adapter": "^2.7.2",
|
||||||
|
"@auth/sveltekit": "^1.4.2",
|
||||||
|
"@prisma/client": "^5.21.1",
|
||||||
|
"lucide-svelte": "^0.447.0",
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
6
sveltekit-integration/postcss.config.js
Normal file
6
sveltekit-integration/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
114
sveltekit-integration/prisma/schema.prisma
Normal file
114
sveltekit-integration/prisma/schema.prisma
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
subdomain String @unique
|
||||||
|
name String?
|
||||||
|
username String @unique
|
||||||
|
image String?
|
||||||
|
emailVerified DateTime?
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
relayTargets RelayTarget[]
|
||||||
|
webhookEvents WebhookEvent[]
|
||||||
|
|
||||||
|
// Optional for WebAuthn support
|
||||||
|
Authenticator Authenticator[]
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Account {
|
||||||
|
userId String
|
||||||
|
type String
|
||||||
|
provider String
|
||||||
|
providerAccountId String
|
||||||
|
refresh_token String?
|
||||||
|
access_token String?
|
||||||
|
expires_at Int?
|
||||||
|
token_type String?
|
||||||
|
scope String?
|
||||||
|
id_token String?
|
||||||
|
session_state String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([provider, providerAccountId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Session {
|
||||||
|
sessionToken String @unique
|
||||||
|
userId String
|
||||||
|
expires DateTime
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model VerificationToken {
|
||||||
|
identifier String
|
||||||
|
token String
|
||||||
|
expires DateTime
|
||||||
|
|
||||||
|
@@id([identifier, token])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional for WebAuthn support
|
||||||
|
model Authenticator {
|
||||||
|
credentialID String @unique
|
||||||
|
userId String
|
||||||
|
providerAccountId String
|
||||||
|
credentialPublicKey String
|
||||||
|
counter Int
|
||||||
|
credentialDeviceType String
|
||||||
|
credentialBackedUp Boolean
|
||||||
|
transports String?
|
||||||
|
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([userId, credentialID])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook Event schema
|
||||||
|
model WebhookEvent {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
path String
|
||||||
|
query String
|
||||||
|
method String
|
||||||
|
body String
|
||||||
|
headers String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhook Relay Target schema
|
||||||
|
model RelayTarget {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
target String
|
||||||
|
nickname String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
active Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([userId, active])
|
||||||
|
}
|
||||||
29
sveltekit-integration/src/app.css
Normal file
29
sveltekit-integration/src/app.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom scrollbar for webkit browsers */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
* {
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
35
sveltekit-integration/src/app.d.ts
vendored
Normal file
35
sveltekit-integration/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { DefaultSession } from '@auth/core/types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
interface Locals {
|
||||||
|
auth: () => Promise<{
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
name?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
image?: string | null;
|
||||||
|
subdomain?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
} | null>;
|
||||||
|
}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@auth/core/types' {
|
||||||
|
interface User {
|
||||||
|
subdomain?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Session extends DefaultSession {
|
||||||
|
user?: User;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
10
sveltekit-integration/src/hooks.server.ts
Normal file
10
sveltekit-integration/src/hooks.server.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { handle as authHandle } from '$auth';
|
||||||
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
const customHandle: Handle = async ({ event, resolve }) => {
|
||||||
|
// Add custom server-side logic here if needed
|
||||||
|
return resolve(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handle = sequence(authHandle, customHandle);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { connectionStatus } from '$lib/stores/webhooks';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
{#if $connectionStatus === 'connected'}
|
||||||
|
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||||
|
{:else if $connectionStatus === 'connecting'}
|
||||||
|
<div class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
|
||||||
|
{:else}
|
||||||
|
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { WebhookEvent } from '$lib/stores/webhooks';
|
||||||
|
|
||||||
|
export let event: WebhookEvent;
|
||||||
|
|
||||||
|
$: formattedTime = new Date(event.createdAt).toLocaleString();
|
||||||
|
$: methodColor = getMethodColor(event.method);
|
||||||
|
|
||||||
|
function getMethodColor(method: string) {
|
||||||
|
switch (method.toLowerCase()) {
|
||||||
|
case 'get': return 'bg-green-100 text-green-800';
|
||||||
|
case 'post': return 'bg-blue-100 text-blue-800';
|
||||||
|
case 'put': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case 'patch': return 'bg-orange-100 text-orange-800';
|
||||||
|
case 'delete': return 'bg-red-100 text-red-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatJson(obj: any) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {methodColor}">
|
||||||
|
{event.method}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-900">{event.path}</span>
|
||||||
|
{#if event.query}
|
||||||
|
<span class="text-xs text-gray-500">{event.query}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="text-xs text-gray-500">{formattedTime}</span>
|
||||||
|
<button
|
||||||
|
on:click={() => expanded = !expanded}
|
||||||
|
class="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 transition-transform"
|
||||||
|
class:rotate-180={expanded}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if expanded}
|
||||||
|
<div class="mt-4 space-y-3">
|
||||||
|
{#if event.body && event.body !== 'null'}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Request Body</h4>
|
||||||
|
<pre class="bg-gray-50 rounded p-3 text-xs overflow-x-auto">{formatJson(event.body)}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if event.headers}
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Headers</h4>
|
||||||
|
<pre class="bg-gray-50 rounded p-3 text-xs overflow-x-auto">{formatJson(event.headers)}</pre>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
53
sveltekit-integration/src/lib/server/auth.ts
Normal file
53
sveltekit-integration/src/lib/server/auth.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { SvelteKitAuth } from '@auth/sveltekit';
|
||||||
|
import GitHub from '@auth/core/providers/github';
|
||||||
|
import { PrismaAdapter } from '@auth/prisma-adapter';
|
||||||
|
import { prisma } from './db';
|
||||||
|
import {
|
||||||
|
GITHUB_CLIENT_ID,
|
||||||
|
GITHUB_CLIENT_SECRET,
|
||||||
|
AUTH_SECRET,
|
||||||
|
REDIRECT_URL
|
||||||
|
} from '$env/static/private';
|
||||||
|
|
||||||
|
export const { handle, signIn, signOut } = SvelteKitAuth({
|
||||||
|
adapter: PrismaAdapter(prisma),
|
||||||
|
providers: [
|
||||||
|
GitHub({
|
||||||
|
clientId: GITHUB_CLIENT_ID,
|
||||||
|
clientSecret: GITHUB_CLIENT_SECRET,
|
||||||
|
profile(profile) {
|
||||||
|
return {
|
||||||
|
id: profile.id.toString(),
|
||||||
|
name: profile.name ?? profile.login,
|
||||||
|
username: profile.login,
|
||||||
|
email: profile.email,
|
||||||
|
image: profile.avatar_url,
|
||||||
|
subdomain: crypto.randomUUID().slice(0, 8), // Shorter subdomain
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
],
|
||||||
|
secret: AUTH_SECRET,
|
||||||
|
trustHost: true,
|
||||||
|
callbacks: {
|
||||||
|
session: async ({ session, user }) => {
|
||||||
|
// Add custom user data to session
|
||||||
|
const dbUser = await prisma.user.findUnique({
|
||||||
|
where: { id: user.id },
|
||||||
|
select: { subdomain: true, username: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dbUser) {
|
||||||
|
session.user.subdomain = dbUser.subdomain;
|
||||||
|
session.user.username = dbUser.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
},
|
||||||
|
redirect: async ({ url, baseUrl }) => {
|
||||||
|
// Redirect to dashboard after sign in
|
||||||
|
if (url.startsWith(baseUrl)) return url;
|
||||||
|
return REDIRECT_URL || `${baseUrl}/dashboard`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
11
sveltekit-integration/src/lib/server/db.ts
Normal file
11
sveltekit-integration/src/lib/server/db.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
// Prevent multiple instances of Prisma Client in development
|
||||||
|
const globalForPrisma = globalThis as unknown as {
|
||||||
|
prisma: PrismaClient | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||||
|
|
||||||
|
if (dev) globalForPrisma.prisma = prisma;
|
||||||
157
sveltekit-integration/src/lib/server/relay.ts
Normal file
157
sveltekit-integration/src/lib/server/relay.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { prisma } from '$db';
|
||||||
|
|
||||||
|
// Store for Server-Sent Events connections
|
||||||
|
const sseConnections = new Map<string, Set<ReadableStreamDefaultController>>();
|
||||||
|
|
||||||
|
export interface WebhookEvent {
|
||||||
|
id: string;
|
||||||
|
type: 'webhook' | 'system';
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an SSE connection for a user
|
||||||
|
*/
|
||||||
|
export function addSSEConnection(userId: string, controller: ReadableStreamDefaultController) {
|
||||||
|
if (!sseConnections.has(userId)) {
|
||||||
|
sseConnections.set(userId, new Set());
|
||||||
|
}
|
||||||
|
sseConnections.get(userId)!.add(controller);
|
||||||
|
|
||||||
|
// Send initial connection message
|
||||||
|
sendSSEMessage(controller, {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
type: 'system',
|
||||||
|
data: { message: 'Connected to webhook relay', timestamp: new Date().toISOString() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an SSE connection for a user
|
||||||
|
*/
|
||||||
|
export function removeSSEConnection(userId: string, controller: ReadableStreamDefaultController) {
|
||||||
|
const userConnections = sseConnections.get(userId);
|
||||||
|
if (userConnections) {
|
||||||
|
userConnections.delete(controller);
|
||||||
|
if (userConnections.size === 0) {
|
||||||
|
sseConnections.delete(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send message to a specific SSE connection
|
||||||
|
*/
|
||||||
|
function sendSSEMessage(controller: ReadableStreamDefaultController, event: WebhookEvent) {
|
||||||
|
try {
|
||||||
|
const message = `data: ${JSON.stringify(event)}\n\n`;
|
||||||
|
controller.enqueue(new TextEncoder().encode(message));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send SSE message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast event to all connections for a specific user
|
||||||
|
*/
|
||||||
|
export async function broadcastToUser(userId: string, event: WebhookEvent): Promise<boolean> {
|
||||||
|
const userConnections = sseConnections.get(userId);
|
||||||
|
|
||||||
|
if (!userConnections || userConnections.size === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
const totalConnections = userConnections.size;
|
||||||
|
|
||||||
|
userConnections.forEach(controller => {
|
||||||
|
try {
|
||||||
|
sendSSEMessage(controller, event);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to broadcast to connection:', error);
|
||||||
|
// Remove failed connection
|
||||||
|
userConnections.delete(controller);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return successCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent webhook events for a user
|
||||||
|
*/
|
||||||
|
export async function getRecentWebhooks(userId: string, limit = 50) {
|
||||||
|
return await prisma.webhookEvent.findMany({
|
||||||
|
where: { userId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: limit,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
method: true,
|
||||||
|
path: true,
|
||||||
|
query: true,
|
||||||
|
body: true,
|
||||||
|
headers: true,
|
||||||
|
createdAt: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get relay targets for a user
|
||||||
|
*/
|
||||||
|
export async function getRelayTargets(userId: string) {
|
||||||
|
return await prisma.relayTarget.findMany({
|
||||||
|
where: { userId, active: true },
|
||||||
|
orderBy: { createdAt: 'desc' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new relay target
|
||||||
|
*/
|
||||||
|
export async function createRelayTarget(userId: string, target: string, nickname?: string) {
|
||||||
|
return await prisma.relayTarget.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
target,
|
||||||
|
nickname,
|
||||||
|
active: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forward webhook to relay targets
|
||||||
|
*/
|
||||||
|
export async function forwardToRelayTargets(userId: string, webhookData: any) {
|
||||||
|
const targets = await getRelayTargets(userId);
|
||||||
|
|
||||||
|
const forwardPromises = targets.map(async (target) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(target.target, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Forwarded-By': 'webhook-relay-sveltekit'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(webhookData)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetId: target.id,
|
||||||
|
success: response.ok,
|
||||||
|
status: response.status
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
targetId: target.id,
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(forwardPromises);
|
||||||
|
}
|
||||||
165
sveltekit-integration/src/lib/stores/webhooks.ts
Normal file
165
sveltekit-integration/src/lib/stores/webhooks.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
export interface WebhookEvent {
|
||||||
|
id: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
query: string;
|
||||||
|
body: any;
|
||||||
|
headers: any;
|
||||||
|
createdAt: string;
|
||||||
|
timestamp?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayTarget {
|
||||||
|
id: string;
|
||||||
|
target: string;
|
||||||
|
nickname?: string;
|
||||||
|
active: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
export const webhookEvents = writable<WebhookEvent[]>([]);
|
||||||
|
export const relayTargets = writable<RelayTarget[]>([]);
|
||||||
|
export const connectionStatus = writable<'connected' | 'disconnected' | 'connecting'>('disconnected');
|
||||||
|
export const isLoading = writable(false);
|
||||||
|
|
||||||
|
// Derived stores
|
||||||
|
export const activeTargets = derived(relayTargets, $targets =>
|
||||||
|
$targets.filter(target => target.active)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const recentEvents = derived(webhookEvents, $events =>
|
||||||
|
$events.slice(0, 10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// SSE Connection management
|
||||||
|
let eventSource: EventSource | null = null;
|
||||||
|
|
||||||
|
export const webhookStore = {
|
||||||
|
// Initialize SSE connection
|
||||||
|
connect: () => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
connectionStatus.set('connecting');
|
||||||
|
|
||||||
|
eventSource = new EventSource('/api/relay/events');
|
||||||
|
|
||||||
|
eventSource.onopen = () => {
|
||||||
|
connectionStatus.set('connected');
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'webhook') {
|
||||||
|
webhookEvents.update(events => [data.data, ...events].slice(0, 100));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse SSE message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = () => {
|
||||||
|
connectionStatus.set('disconnected');
|
||||||
|
// Attempt to reconnect after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (eventSource?.readyState === EventSource.CLOSED) {
|
||||||
|
webhookStore.connect();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disconnect SSE
|
||||||
|
disconnect: () => {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
eventSource = null;
|
||||||
|
}
|
||||||
|
connectionStatus.set('disconnected');
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load initial webhook history
|
||||||
|
loadHistory: async () => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
isLoading.set(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/webhooks');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
webhookEvents.set(data.webhooks);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load webhook history:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Load relay targets
|
||||||
|
loadTargets: async () => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/relay/targets');
|
||||||
|
if (response.ok) {
|
||||||
|
const targets = await response.json();
|
||||||
|
relayTargets.set(targets);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load relay targets:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add relay target
|
||||||
|
addTarget: async (target: string, nickname?: string) => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/relay/targets', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ target, nickname })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const newTarget = await response.json();
|
||||||
|
relayTargets.update(targets => [...targets, newTarget]);
|
||||||
|
return newTarget;
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || 'Failed to add target');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add relay target:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove relay target
|
||||||
|
removeTarget: async (targetId: string) => {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/relay/targets', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ targetId })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
relayTargets.update(targets =>
|
||||||
|
targets.filter(target => target.id !== targetId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove relay target:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
9
sveltekit-integration/src/routes/+layout.server.ts
Normal file
9
sveltekit-integration/src/routes/+layout.server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
return {
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
||||||
89
sveltekit-integration/src/routes/+layout.svelte
Normal file
89
sveltekit-integration/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { webhookStore } from '$lib/stores/webhooks';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Initialize webhook store connection if user is authenticated
|
||||||
|
if (data.session?.user) {
|
||||||
|
webhookStore.connect();
|
||||||
|
webhookStore.loadHistory();
|
||||||
|
webhookStore.loadTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
webhookStore.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive cleanup when session changes
|
||||||
|
$: if (!data.session?.user) {
|
||||||
|
webhookStore.disconnect();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
{#if data.session?.user}
|
||||||
|
<nav class="bg-white shadow-sm border-b">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-between h-16">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="/dashboard" class="text-xl font-bold text-gray-900">
|
||||||
|
Webhook Relay
|
||||||
|
</a>
|
||||||
|
<div class="ml-8 flex space-x-4">
|
||||||
|
<a
|
||||||
|
href="/dashboard"
|
||||||
|
class="px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
class:bg-gray-100={$page.url.pathname === '/dashboard'}
|
||||||
|
class:text-gray-900={$page.url.pathname === '/dashboard'}
|
||||||
|
class:text-gray-500={$page.url.pathname !== '/dashboard'}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/dashboard/webhooks"
|
||||||
|
class="px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
class:bg-gray-100={$page.url.pathname.startsWith('/dashboard/webhooks')}
|
||||||
|
class:text-gray-900={$page.url.pathname.startsWith('/dashboard/webhooks')}
|
||||||
|
class:text-gray-500={!$page.url.pathname.startsWith('/dashboard/webhooks')}
|
||||||
|
>
|
||||||
|
Webhooks
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/dashboard/targets"
|
||||||
|
class="px-3 py-2 rounded-md text-sm font-medium"
|
||||||
|
class:bg-gray-100={$page.url.pathname.startsWith('/dashboard/targets')}
|
||||||
|
class:text-gray-900={$page.url.pathname.startsWith('/dashboard/targets')}
|
||||||
|
class:text-gray-500={!$page.url.pathname.startsWith('/dashboard/targets')}
|
||||||
|
>
|
||||||
|
Relay Targets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<span class="text-sm text-gray-700">
|
||||||
|
{data.session.user.name || data.session.user.username}
|
||||||
|
</span>
|
||||||
|
<form action="/auth/signout" method="post">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="bg-gray-200 hover:bg-gray-300 px-4 py-2 rounded-md text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
65
sveltekit-integration/src/routes/+page.svelte
Normal file
65
sveltekit-integration/src/routes/+page.svelte
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { signIn } from '@auth/sveltekit/client';
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Webhook Relay - SvelteKit</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if data.session?.user}
|
||||||
|
<!-- Redirect authenticated users to dashboard -->
|
||||||
|
<script>
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
</script>
|
||||||
|
{:else}
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
Webhook Relay
|
||||||
|
</h2>
|
||||||
|
<p class="mt-2 text-center text-sm text-gray-600">
|
||||||
|
Receive, monitor, and relay webhooks in real-time
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 space-y-6">
|
||||||
|
<div class="rounded-md shadow-sm space-y-4">
|
||||||
|
<div class="bg-white p-6 rounded-lg shadow">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900 mb-4">Features</h3>
|
||||||
|
<ul class="space-y-2 text-sm text-gray-600">
|
||||||
|
<li class="flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-3"></span>
|
||||||
|
Real-time webhook monitoring
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-3"></span>
|
||||||
|
Subdomain-based routing
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-3"></span>
|
||||||
|
Multiple relay targets
|
||||||
|
</li>
|
||||||
|
<li class="flex items-center">
|
||||||
|
<span class="w-2 h-2 bg-green-500 rounded-full mr-3"></span>
|
||||||
|
Event history and logging
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
on:click={() => signIn('github')}
|
||||||
|
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-gray-800 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
Sign in with GitHub
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
49
sveltekit-integration/src/routes/api/relay/events/+server.ts
Normal file
49
sveltekit-integration/src/routes/api/relay/events/+server.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { addSSEConnection, removeSSEConnection } from '$lib/server/relay';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id;
|
||||||
|
|
||||||
|
// Create Server-Sent Events stream
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
// Add this connection to our tracking
|
||||||
|
addSSEConnection(userId, controller);
|
||||||
|
|
||||||
|
// Set up periodic heartbeat to keep connection alive
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
try {
|
||||||
|
controller.enqueue(new TextEncoder().encode(': heartbeat\n\n'));
|
||||||
|
} catch (error) {
|
||||||
|
clearInterval(heartbeat);
|
||||||
|
}
|
||||||
|
}, 30000); // 30 seconds
|
||||||
|
|
||||||
|
// Clean up on stream close
|
||||||
|
return () => {
|
||||||
|
clearInterval(heartbeat);
|
||||||
|
removeSSEConnection(userId, controller);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
removeSSEConnection(userId, this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'Cache-Control'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { prisma } from '$db';
|
||||||
|
import { createRelayTarget, getRelayTargets } from '$lib/server/relay';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const createTargetSchema = z.object({
|
||||||
|
target: z.string().url('Invalid URL format'),
|
||||||
|
nickname: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targets = await getRelayTargets(session.user.id);
|
||||||
|
return json(targets);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { target, nickname } = createTargetSchema.parse(body);
|
||||||
|
|
||||||
|
const newTarget = await createRelayTarget(session.user.id, target, nickname);
|
||||||
|
|
||||||
|
return json(newTarget, { status: 201 });
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
throw error(400, err.errors[0].message);
|
||||||
|
}
|
||||||
|
throw error(500, 'Failed to create relay target');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE: RequestHandler = async ({ request, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { targetId } = await request.json();
|
||||||
|
|
||||||
|
const target = await prisma.relayTarget.findFirst({
|
||||||
|
where: {
|
||||||
|
id: targetId,
|
||||||
|
userId: session.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw error(404, 'Relay target not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.relayTarget.update({
|
||||||
|
where: { id: targetId },
|
||||||
|
data: { active: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
throw error(500, 'Failed to delete relay target');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { prisma } from '$db';
|
||||||
|
import { broadcastToUser } from '$lib/server/relay';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, params, url }) => {
|
||||||
|
const { subdomain } = params;
|
||||||
|
|
||||||
|
if (!subdomain) {
|
||||||
|
throw error(400, 'Missing subdomain');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find user by subdomain
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { subdomain }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw error(404, 'Invalid subdomain');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request body
|
||||||
|
let body: any = null;
|
||||||
|
const contentType = request.headers.get('content-type');
|
||||||
|
|
||||||
|
if (contentType?.includes('application/json')) {
|
||||||
|
body = await request.json();
|
||||||
|
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = await request.formData();
|
||||||
|
body = Object.fromEntries(formData);
|
||||||
|
} else {
|
||||||
|
body = await request.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect headers (excluding sensitive ones)
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
request.headers.forEach((value, key) => {
|
||||||
|
if (!key.toLowerCase().includes('authorization') &&
|
||||||
|
!key.toLowerCase().includes('cookie')) {
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create webhook event record
|
||||||
|
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 savedEvent = await prisma.webhookEvent.create({
|
||||||
|
data: webhookEvent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast to connected clients via SSE
|
||||||
|
const broadcastSuccess = await broadcastToUser(user.id, {
|
||||||
|
id: savedEvent.id,
|
||||||
|
type: 'webhook',
|
||||||
|
data: {
|
||||||
|
...webhookEvent,
|
||||||
|
timestamp: savedEvent.createdAt.toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
logged: true,
|
||||||
|
forwarded: broadcastSuccess,
|
||||||
|
subdomain,
|
||||||
|
eventId: savedEvent.id
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Webhook processing error:', err);
|
||||||
|
throw error(500, 'Failed to process webhook');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle other HTTP methods
|
||||||
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
|
return json({
|
||||||
|
message: `Webhook endpoint for ${params.subdomain}`,
|
||||||
|
methods: ['POST'],
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PUT = POST;
|
||||||
|
export const PATCH = POST;
|
||||||
|
export const DELETE = POST;
|
||||||
22
sveltekit-integration/src/routes/api/webhooks/+server.ts
Normal file
22
sveltekit-integration/src/routes/api/webhooks/+server.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { getRecentWebhooks } from '$lib/server/relay';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'Unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
||||||
|
const webhooks = await getRecentWebhooks(session.user.id, limit);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
webhooks: webhooks.map(webhook => ({
|
||||||
|
...webhook,
|
||||||
|
body: webhook.body ? JSON.parse(webhook.body) : null,
|
||||||
|
headers: webhook.headers ? JSON.parse(webhook.headers) : null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { handle as GET, handle as POST } from '$auth';
|
||||||
12
sveltekit-integration/src/routes/auth/signin/+page.server.ts
Normal file
12
sveltekit-integration/src/routes/auth/signin/+page.server.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (session?.user) {
|
||||||
|
throw redirect(303, '/dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
14
sveltekit-integration/src/routes/dashboard/+layout.server.ts
Normal file
14
sveltekit-integration/src/routes/dashboard/+layout.server.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
throw redirect(303, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
session
|
||||||
|
};
|
||||||
|
};
|
||||||
132
sveltekit-integration/src/routes/dashboard/+page.svelte
Normal file
132
sveltekit-integration/src/routes/dashboard/+page.svelte
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { webhookEvents, connectionStatus, recentEvents } from '$lib/stores/webhooks';
|
||||||
|
import ConnectionStatus from '$lib/components/ConnectionStatus.svelte';
|
||||||
|
import WebhookEventCard from '$lib/components/WebhookEventCard.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
$: user = data.session?.user;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Dashboard - Webhook Relay</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-8">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Webhook Dashboard</h1>
|
||||||
|
<p class="mt-2 text-gray-600">
|
||||||
|
Monitor and manage your webhook endpoints in real-time
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Info & Connection Status -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<img class="h-10 w-10 rounded-full" src={user?.image} alt={user?.name} />
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">Subdomain</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900">{user?.subdomain}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<ConnectionStatus />
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">Connection</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900 capitalize">{$connectionStatus}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
|
<div class="p-5">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-white text-sm font-medium">{$webhookEvents.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-5 w-0 flex-1">
|
||||||
|
<dl>
|
||||||
|
<dt class="text-sm font-medium text-gray-500 truncate">Total Events</dt>
|
||||||
|
<dd class="text-lg font-medium text-gray-900">Today</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Webhook Endpoint Info -->
|
||||||
|
<div class="bg-white shadow rounded-lg mb-8">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Your Webhook Endpoint
|
||||||
|
</h3>
|
||||||
|
<div class="bg-gray-50 rounded-md p-4">
|
||||||
|
<code class="text-sm text-gray-800">
|
||||||
|
POST https://yourdomain.com/api/webhook/{user?.subdomain}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">
|
||||||
|
Send webhooks to this endpoint. All events will be logged and forwarded to your connected relay targets.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Events -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Recent Webhook Events
|
||||||
|
</h3>
|
||||||
|
<a
|
||||||
|
href="/dashboard/webhooks"
|
||||||
|
class="text-sm text-blue-600 hover:text-blue-500"
|
||||||
|
>
|
||||||
|
View all →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $recentEvents.length > 0}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each $recentEvents as event (event.id)}
|
||||||
|
<WebhookEventCard {event} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<div class="text-gray-400 mb-2">
|
||||||
|
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2 2v-5m16 0h-5.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">No webhook events yet</h3>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Send a test webhook to your endpoint to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
192
sveltekit-integration/src/routes/dashboard/targets/+page.svelte
Normal file
192
sveltekit-integration/src/routes/dashboard/targets/+page.svelte
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { relayTargets, webhookStore } from '$lib/stores/webhooks';
|
||||||
|
import { Plus, ExternalLink, Trash2 } from 'lucide-svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let showAddForm = false;
|
||||||
|
let newTarget = '';
|
||||||
|
let newNickname = '';
|
||||||
|
let isSubmitting = false;
|
||||||
|
|
||||||
|
async function addTarget() {
|
||||||
|
if (!newTarget.trim()) return;
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
try {
|
||||||
|
await webhookStore.addTarget(newTarget.trim(), newNickname.trim() || undefined);
|
||||||
|
newTarget = '';
|
||||||
|
newNickname = '';
|
||||||
|
showAddForm = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add target:', error);
|
||||||
|
alert('Failed to add relay target. Please check the URL and try again.');
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTarget(targetId: string) {
|
||||||
|
if (!confirm('Are you sure you want to remove this relay target?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await webhookStore.removeTarget(targetId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove target:', error);
|
||||||
|
alert('Failed to remove relay target.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Relay Targets - Webhook Relay</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Relay Targets</h1>
|
||||||
|
<p class="mt-2 text-gray-600">
|
||||||
|
Configure where your webhooks should be forwarded
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
on:click={() => showAddForm = !showAddForm}
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
|
Add Target
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add Target Form -->
|
||||||
|
{#if showAddForm}
|
||||||
|
<div class="bg-white shadow rounded-lg mb-8">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Add New Relay Target
|
||||||
|
</h3>
|
||||||
|
<form on:submit|preventDefault={addTarget} class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="target-url" class="block text-sm font-medium text-gray-700">
|
||||||
|
Target URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="target-url"
|
||||||
|
type="url"
|
||||||
|
bind:value={newTarget}
|
||||||
|
placeholder="https://example.com/webhook"
|
||||||
|
required
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="nickname" class="block text-sm font-medium text-gray-700">
|
||||||
|
Nickname (Optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="nickname"
|
||||||
|
type="text"
|
||||||
|
bind:value={newNickname}
|
||||||
|
placeholder="My API Server"
|
||||||
|
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={() => showAddForm = false}
|
||||||
|
class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
class="bg-blue-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Adding...' : 'Add Target'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Targets List -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Active Relay Targets ({$relayTargets.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{#if $relayTargets.length > 0}
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each $relayTargets as target (target.id)}
|
||||||
|
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="flex-1">
|
||||||
|
{#if target.nickname}
|
||||||
|
<h4 class="text-sm font-medium text-gray-900">{target.nickname}</h4>
|
||||||
|
<p class="text-sm text-gray-500">{target.target}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm font-medium text-gray-900">{target.target}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
Active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
Added {new Date(target.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
|
<a
|
||||||
|
href={target.target}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-gray-400 hover:text-gray-600"
|
||||||
|
title="Open in new tab"
|
||||||
|
>
|
||||||
|
<ExternalLink class="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
on:click={() => removeTarget(target.id)}
|
||||||
|
class="text-red-400 hover:text-red-600"
|
||||||
|
title="Remove target"
|
||||||
|
>
|
||||||
|
<Trash2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<div class="text-gray-400 mb-2">
|
||||||
|
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">No relay targets configured</h3>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">
|
||||||
|
Add relay targets to forward incoming webhooks to your services.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
on:click={() => showAddForm = true}
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-600 bg-blue-100 hover:bg-blue-200"
|
||||||
|
>
|
||||||
|
<Plus class="w-4 h-4 mr-2" />
|
||||||
|
Add Your First Target
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
122
sveltekit-integration/src/routes/dashboard/webhooks/+page.svelte
Normal file
122
sveltekit-integration/src/routes/dashboard/webhooks/+page.svelte
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { webhookEvents, isLoading } from '$lib/stores/webhooks';
|
||||||
|
import WebhookEventCard from '$lib/components/WebhookEventCard.svelte';
|
||||||
|
import ConnectionStatus from '$lib/components/ConnectionStatus.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
$: user = data.session?.user;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Webhooks - Webhook Relay</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
|
<div class="px-4 py-6 sm:px-0">
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">Webhook Events</h1>
|
||||||
|
<p class="mt-2 text-gray-600">
|
||||||
|
Real-time view of all incoming webhook events
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<ConnectionStatus />
|
||||||
|
<span class="text-sm text-gray-500">Live updates</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Webhook Endpoint Info -->
|
||||||
|
<div class="bg-white shadow rounded-lg mb-8">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Your Webhook Endpoint
|
||||||
|
</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="bg-gray-50 rounded-md p-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<code class="text-sm text-gray-800">
|
||||||
|
POST https://yourdomain.com/api/webhook/{user?.subdomain}
|
||||||
|
</code>
|
||||||
|
<button
|
||||||
|
on:click={() => navigator.clipboard.writeText(`https://yourdomain.com/api/webhook/${user?.subdomain}`)}
|
||||||
|
class="text-xs text-blue-600 hover:text-blue-500"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Configure external services to send webhooks to this endpoint.
|
||||||
|
All events will be logged and forwarded to your relay targets.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test Webhook -->
|
||||||
|
<div class="bg-white shadow rounded-lg mb-8">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Test Your Webhook
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
on:click={async () => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/webhook/${user?.subdomain}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
test: true,
|
||||||
|
message: 'Test webhook from dashboard',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send test webhook:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
||||||
|
>
|
||||||
|
Send Test Webhook
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Events List -->
|
||||||
|
<div class="bg-white shadow rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
||||||
|
Event History ({$webhookEvents.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{#if $isLoading}
|
||||||
|
<div class="flex justify-center py-8">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
{:else if $webhookEvents.length > 0}
|
||||||
|
<div class="space-y-4 max-h-96 overflow-y-auto">
|
||||||
|
{#each $webhookEvents as event (event.id)}
|
||||||
|
<WebhookEventCard {event} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<div class="text-gray-400 mb-2">
|
||||||
|
<svg class="mx-auto h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2 2v-5m16 0h-5.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">No webhook events yet</h3>
|
||||||
|
<p class="text-sm text-gray-500 mb-4">
|
||||||
|
Send a webhook to your endpoint or use the test button above.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
sveltekit-integration/svelte.config.js
Normal file
22
sveltekit-integration/svelte.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||||
|
// for more information about preprocessors
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
|
kit: {
|
||||||
|
// 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.
|
||||||
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
|
adapter: adapter(),
|
||||||
|
alias: {
|
||||||
|
$db: './src/lib/server/db',
|
||||||
|
$auth: './src/lib/server/auth'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
8
sveltekit-integration/tailwind.config.js
Normal file
8
sveltekit-integration/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
10
sveltekit-integration/vite.config.ts
Normal file
10
sveltekit-integration/vite.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user