mirror of
https://github.com/LukeHagar/relay.git
synced 2025-12-09 12:47:49 +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