mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 03:26:44 +03:00
Updated some interfaces to be more future proof
This commit is contained in:
50
.env.example
50
.env.example
@@ -1,21 +1,40 @@
|
||||
# Docker Registry Configuration
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_IMAGE=arunavo4/gitea-mirror
|
||||
DOCKER_TAG=latest
|
||||
# Gitea Mirror Configuration
|
||||
# Copy this to .env and update with your values
|
||||
|
||||
# ===========================================
|
||||
# CORE CONFIGURATION
|
||||
# ===========================================
|
||||
|
||||
# Application Configuration
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=4321
|
||||
|
||||
# Database Configuration
|
||||
# For self-hosted, SQLite is used by default
|
||||
DATABASE_URL=sqlite://data/gitea-mirror.db
|
||||
|
||||
# Security
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
||||
BETTER_AUTH_URL=http://localhost:4321
|
||||
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
|
||||
|
||||
# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI)
|
||||
# Uncomment and set as needed. These are passed as environment variables to the container.
|
||||
# ===========================================
|
||||
# DOCKER CONFIGURATION (Optional)
|
||||
# ===========================================
|
||||
|
||||
# Docker Registry Configuration
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_IMAGE=arunavo4/gitea-mirror
|
||||
DOCKER_TAG=latest
|
||||
|
||||
# ===========================================
|
||||
# MIRROR CONFIGURATION (Optional)
|
||||
# Can also be configured via web UI
|
||||
# ===========================================
|
||||
|
||||
# GitHub Configuration
|
||||
# GITHUB_USERNAME=your-github-username
|
||||
# GITHUB_TOKEN=your-github-personal-access-token
|
||||
# SKIP_FORKS=false
|
||||
@@ -27,6 +46,8 @@ BETTER_AUTH_URL=http://localhost:4321
|
||||
# PRESERVE_ORG_STRUCTURE=false
|
||||
# ONLY_MIRROR_ORGS=false
|
||||
# SKIP_STARRED_ISSUES=false
|
||||
|
||||
# Gitea Configuration
|
||||
# GITEA_URL=http://gitea:3000
|
||||
# GITEA_TOKEN=your-local-gitea-token
|
||||
# GITEA_USERNAME=your-local-gitea-username
|
||||
@@ -34,15 +55,14 @@ BETTER_AUTH_URL=http://localhost:4321
|
||||
# GITEA_ORG_VISIBILITY=public
|
||||
# DELAY=3600
|
||||
|
||||
# Optional Database Cleanup Configuration (configured via web UI)
|
||||
# These environment variables are optional and only used as defaults
|
||||
# Users can configure cleanup settings through the web interface
|
||||
# ===========================================
|
||||
# OPTIONAL FEATURES
|
||||
# ===========================================
|
||||
|
||||
# Database Cleanup Configuration
|
||||
# CLEANUP_ENABLED=false
|
||||
# CLEANUP_RETENTION_DAYS=7
|
||||
|
||||
# Optional TLS/SSL Configuration
|
||||
# Option 1: Mount custom CA certificates in ./certs directory as .crt files
|
||||
# The container will automatically combine them into a CA bundle
|
||||
# Option 2: Mount your system CA bundle at /etc/ssl/certs/ca-certificates.crt
|
||||
# See docker-compose.yml for volume mount examples
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing, disables TLS verification
|
||||
# TLS/SSL Configuration
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,5 +32,3 @@ certs/*.pem
|
||||
certs/*.cer
|
||||
!certs/README.md
|
||||
|
||||
# Hosted version documentation (local only)
|
||||
docs/HOSTED_VERSION.md
|
||||
|
||||
91
docs/SPONSOR_INTEGRATION.md
Normal file
91
docs/SPONSOR_INTEGRATION.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# GitHub Sponsors Integration
|
||||
|
||||
This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror.
|
||||
|
||||
## Components
|
||||
|
||||
### GitHubSponsors Card
|
||||
|
||||
A card component that displays in the sidebar or dashboard:
|
||||
|
||||
```tsx
|
||||
import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your layout or dashboard
|
||||
<GitHubSponsors />
|
||||
```
|
||||
|
||||
### SponsorButton
|
||||
|
||||
A smaller button for headers or navigation:
|
||||
|
||||
```tsx
|
||||
import { SponsorButton } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your header
|
||||
<SponsorButton />
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. Dashboard Sidebar
|
||||
|
||||
Add the sponsor card to the dashboard sidebar for visibility:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/DashboardLayout.tsx
|
||||
<aside>
|
||||
{/* Other sidebar content */}
|
||||
<GitHubSponsors />
|
||||
</aside>
|
||||
```
|
||||
|
||||
### 2. Header Navigation
|
||||
|
||||
Add the sponsor button to the main navigation:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/Header.tsx
|
||||
<nav>
|
||||
{/* Other nav items */}
|
||||
<SponsorButton />
|
||||
</nav>
|
||||
```
|
||||
|
||||
### 3. Settings Page
|
||||
|
||||
Add a support section in settings:
|
||||
|
||||
```tsx
|
||||
// src/components/settings/SupportSection.tsx
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support Development</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GitHubSponsors />
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Only appears in self-hosted mode**: The components automatically hide in hosted mode
|
||||
- **Non-intrusive**: Designed to be helpful without being annoying
|
||||
- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo
|
||||
|
||||
## Customization
|
||||
|
||||
You can customize the sponsor components by:
|
||||
|
||||
1. Updating the GitHub Sponsors URL
|
||||
2. Adding/removing donation platforms
|
||||
3. Changing the styling to match your theme
|
||||
4. Adjusting the placement based on user feedback
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Don't be pushy**: Show sponsor options tastefully
|
||||
2. **Provide value first**: Ensure the tool is useful before asking for support
|
||||
3. **Be transparent**: Explain how sponsorships help the project
|
||||
4. **Thank sponsors**: Acknowledge supporters in README or releases
|
||||
9087
package-lock.json
generated
Normal file
9087
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
72
src/components/layout/SponsorCard.tsx
Normal file
72
src/components/layout/SponsorCard.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Heart, Coffee, Sparkles } from "lucide-react";
|
||||
import { isSelfHostedMode } from "@/lib/deployment-mode";
|
||||
|
||||
export function SponsorCard() {
|
||||
// Only show in self-hosted mode
|
||||
if (!isSelfHostedMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-auto p-4 border-t">
|
||||
<Card className="bg-gradient-to-r from-purple-500/10 to-pink-500/10 border-purple-500/20">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||
<Heart className="w-4 h-4 text-pink-500" />
|
||||
Support Development
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Help us improve Gitea Mirror
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Gitea Mirror is open source and free. Your sponsorship helps us maintain and improve it.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
className="w-full h-8 text-xs"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href="https://github.com/sponsors/RayLabsHQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Heart className="w-3 h-3 mr-2" />
|
||||
Sponsor on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full h-8 text-xs"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href="https://buymeacoffee.com/raylabs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Coffee className="w-3 h-3 mr-2" />
|
||||
Buy us a coffee
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Pro features available in hosted version
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
105
src/components/sponsors/GitHubSponsors.tsx
Normal file
105
src/components/sponsors/GitHubSponsors.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Heart, Coffee, Zap } from "lucide-react";
|
||||
import { isSelfHostedMode } from "@/lib/deployment-mode";
|
||||
|
||||
export function GitHubSponsors() {
|
||||
// Only show in self-hosted mode
|
||||
if (!isSelfHostedMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-purple-200 dark:border-purple-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-purple-900 dark:text-purple-100">
|
||||
<Heart className="w-5 h-5 text-pink-500" />
|
||||
Support Gitea Mirror
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-purple-800 dark:text-purple-200">
|
||||
Gitea Mirror is open source and free to use. If you find it helpful,
|
||||
consider supporting the project!
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href="https://github.com/sponsors/RayLabsHQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Heart className="w-4 h-4 mr-2" />
|
||||
Become a Sponsor
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-purple-300 hover:bg-purple-100 dark:border-purple-700 dark:hover:bg-purple-900"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href="https://github.com/RayLabsHQ/gitea-mirror"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
⭐ Star on GitHub
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-purple-300 hover:bg-purple-100 dark:border-purple-700 dark:hover:bg-purple-900"
|
||||
asChild
|
||||
>
|
||||
<a
|
||||
href="https://buymeacoffee.com/raylabs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Coffee className="w-4 h-4 mr-1" />
|
||||
Buy Coffee
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-purple-600 dark:text-purple-300 space-y-1">
|
||||
<p className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
Your support helps maintain and improve the project
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Smaller inline sponsor button for headers/navbars
|
||||
export function SponsorButton() {
|
||||
if (!isSelfHostedMode()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a
|
||||
href="https://github.com/sponsors/RayLabsHQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Heart className="w-4 h-4 mr-2" />
|
||||
Sponsor
|
||||
</a>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { oidcProvider } from "better-auth/plugins";
|
||||
import { sso } from "better-auth/plugins/sso";
|
||||
import { oidcProvider, sso } from "better-auth/plugins";
|
||||
import { db, users } from "./db";
|
||||
import * as schema from "./db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
102
src/lib/db/adapter.ts
Normal file
102
src/lib/db/adapter.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Database adapter for SQLite
|
||||
* For the self-hosted version of Gitea Mirror
|
||||
*/
|
||||
|
||||
import { drizzle as drizzleSqlite } from 'drizzle-orm/bun-sqlite';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import * as schema from './schema';
|
||||
|
||||
export type DatabaseClient = ReturnType<typeof createDatabase>;
|
||||
|
||||
/**
|
||||
* Create SQLite database connection
|
||||
*/
|
||||
export function createDatabase() {
|
||||
const dbPath = process.env.DATABASE_PATH || './data/gitea-mirror.db';
|
||||
|
||||
// Ensure directory exists
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Create SQLite connection
|
||||
const sqlite = new Database(dbPath);
|
||||
|
||||
// Enable foreign keys and WAL mode for better performance
|
||||
sqlite.exec('PRAGMA foreign_keys = ON');
|
||||
sqlite.exec('PRAGMA journal_mode = WAL');
|
||||
sqlite.exec('PRAGMA synchronous = NORMAL');
|
||||
sqlite.exec('PRAGMA cache_size = -2000'); // 2MB cache
|
||||
sqlite.exec('PRAGMA temp_store = MEMORY');
|
||||
|
||||
// Create Drizzle instance with SQLite
|
||||
const db = drizzleSqlite(sqlite, {
|
||||
schema,
|
||||
logger: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
return {
|
||||
db,
|
||||
client: sqlite,
|
||||
type: 'sqlite' as const,
|
||||
|
||||
// Helper methods
|
||||
async close() {
|
||||
sqlite.close();
|
||||
},
|
||||
|
||||
async healthCheck() {
|
||||
try {
|
||||
sqlite.query('SELECT 1').get();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
async transaction<T>(fn: (tx: any) => Promise<T>) {
|
||||
return db.transaction(fn);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
let dbInstance: DatabaseClient | null = null;
|
||||
|
||||
/**
|
||||
* Get database instance (singleton)
|
||||
*/
|
||||
export function getDatabase(): DatabaseClient {
|
||||
if (!dbInstance) {
|
||||
dbInstance = createDatabase();
|
||||
}
|
||||
return dbInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
export async function closeDatabase() {
|
||||
if (dbInstance) {
|
||||
await dbInstance.close();
|
||||
dbInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Export convenience references
|
||||
export const { db, client, type: dbType } = getDatabase();
|
||||
|
||||
// Re-export schema for convenience
|
||||
export * from './schema';
|
||||
|
||||
/**
|
||||
* Database migration utilities
|
||||
*/
|
||||
export async function runMigrations() {
|
||||
const { migrate } = await import('drizzle-orm/bun-sqlite/migrator');
|
||||
await migrate(db, { migrationsFolder: './drizzle' });
|
||||
}
|
||||
21
src/lib/deployment-mode.ts
Normal file
21
src/lib/deployment-mode.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Deployment mode utilities
|
||||
* For the open source self-hosted version
|
||||
*/
|
||||
|
||||
export const DEPLOYMENT_MODE = 'selfhosted';
|
||||
|
||||
export const isSelfHostedMode = () => true;
|
||||
|
||||
/**
|
||||
* Feature flags for self-hosted version
|
||||
*/
|
||||
export const features = {
|
||||
// Core features available
|
||||
githubSync: true,
|
||||
giteaMirroring: true,
|
||||
scheduling: true,
|
||||
multiUser: true,
|
||||
githubSponsors: true,
|
||||
unlimitedRepos: true,
|
||||
};
|
||||
256
src/lib/events/realtime.ts
Normal file
256
src/lib/events/realtime.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Real-time event system using EventEmitter
|
||||
* For the self-hosted version
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface RealtimeEvent {
|
||||
type: string;
|
||||
userId?: string;
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real-time event bus for local instance
|
||||
*/
|
||||
export class RealtimeEventBus extends EventEmitter {
|
||||
private channels = new Map<string, Set<(event: RealtimeEvent) => void>>();
|
||||
private userChannels = new Map<string, string[]>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming events
|
||||
*/
|
||||
private handleIncomingEvent(channel: string, event: RealtimeEvent) {
|
||||
// Emit to local listeners
|
||||
this.emit(channel, event);
|
||||
|
||||
// Call channel-specific handlers
|
||||
const handlers = this.channels.get(channel);
|
||||
if (handlers) {
|
||||
handlers.forEach(handler => {
|
||||
try {
|
||||
handler(event);
|
||||
} catch (error) {
|
||||
console.error('Error in event handler:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a channel
|
||||
*/
|
||||
async subscribe(channel: string, handler?: (event: RealtimeEvent) => void) {
|
||||
// Add handler if provided
|
||||
if (handler) {
|
||||
if (!this.channels.has(channel)) {
|
||||
this.channels.set(channel, new Set());
|
||||
}
|
||||
this.channels.get(channel)!.add(handler);
|
||||
}
|
||||
|
||||
// Add local listener
|
||||
if (!this.listenerCount(channel)) {
|
||||
this.on(channel, (event) => this.handleIncomingEvent(channel, event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to user-specific channels
|
||||
*/
|
||||
async subscribeUser(userId: string) {
|
||||
const channels = [
|
||||
`user:${userId}`,
|
||||
`user:${userId}:notifications`,
|
||||
`user:${userId}:updates`,
|
||||
];
|
||||
|
||||
this.userChannels.set(userId, channels);
|
||||
|
||||
for (const channel of channels) {
|
||||
await this.subscribe(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from a channel
|
||||
*/
|
||||
async unsubscribe(channel: string, handler?: (event: RealtimeEvent) => void) {
|
||||
// Remove handler if provided
|
||||
if (handler) {
|
||||
this.channels.get(channel)?.delete(handler);
|
||||
|
||||
// Remove channel if no handlers left
|
||||
if (this.channels.get(channel)?.size === 0) {
|
||||
this.channels.delete(channel);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove local listener if no handlers
|
||||
if (!this.channels.has(channel)) {
|
||||
this.removeAllListeners(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from user channels
|
||||
*/
|
||||
async unsubscribeUser(userId: string) {
|
||||
const channels = this.userChannels.get(userId) || [];
|
||||
|
||||
for (const channel of channels) {
|
||||
await this.unsubscribe(channel);
|
||||
}
|
||||
|
||||
this.userChannels.delete(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event
|
||||
*/
|
||||
async publish(channel: string, event: Omit<RealtimeEvent, 'timestamp'>) {
|
||||
const fullEvent: RealtimeEvent = {
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Emit locally
|
||||
this.handleIncomingEvent(channel, fullEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast to all users
|
||||
*/
|
||||
async broadcast(event: Omit<RealtimeEvent, 'timestamp'>) {
|
||||
await this.publish('broadcast', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send event to specific user
|
||||
*/
|
||||
async sendToUser(userId: string, event: Omit<RealtimeEvent, 'timestamp' | 'userId'>) {
|
||||
await this.publish(`user:${userId}`, {
|
||||
...event,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send activity update
|
||||
*/
|
||||
async sendActivity(activity: {
|
||||
userId: string;
|
||||
action: string;
|
||||
resource: string;
|
||||
resourceId: string;
|
||||
details?: any;
|
||||
}) {
|
||||
const event = {
|
||||
type: 'activity',
|
||||
data: activity,
|
||||
};
|
||||
|
||||
// Send to user
|
||||
await this.sendToUser(activity.userId, event);
|
||||
|
||||
// Also publish to activity channel
|
||||
await this.publish('activity', {
|
||||
...event,
|
||||
userId: activity.userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get event statistics
|
||||
*/
|
||||
getStats() {
|
||||
return {
|
||||
channels: this.channels.size,
|
||||
listeners: Array.from(this.channels.values()).reduce(
|
||||
(sum, handlers) => sum + handlers.size,
|
||||
0
|
||||
),
|
||||
userChannels: this.userChannels.size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Global event bus instance
|
||||
export const eventBus = new RealtimeEventBus();
|
||||
|
||||
/**
|
||||
* React hook for subscribing to events
|
||||
*/
|
||||
export function useRealtimeEvents(
|
||||
channel: string,
|
||||
handler: (event: RealtimeEvent) => void,
|
||||
deps: any[] = []
|
||||
) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const { useEffect } = require('react');
|
||||
|
||||
useEffect(() => {
|
||||
eventBus.subscribe(channel, handler);
|
||||
|
||||
return () => {
|
||||
eventBus.unsubscribe(channel, handler);
|
||||
};
|
||||
}, deps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-sent events endpoint handler
|
||||
*/
|
||||
export async function createSSEHandler(userId: string) {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
// Create a readable stream for SSE
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// Send initial connection event
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
|
||||
);
|
||||
|
||||
// Subscribe to user channels
|
||||
await eventBus.subscribeUser(userId);
|
||||
|
||||
// Create event handler
|
||||
const handleEvent = (event: RealtimeEvent) => {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify(event)}\n\n`)
|
||||
);
|
||||
};
|
||||
|
||||
// Subscribe to channels
|
||||
eventBus.on(`user:${userId}`, handleEvent);
|
||||
|
||||
// Keep connection alive with heartbeat
|
||||
const heartbeat = setInterval(() => {
|
||||
controller.enqueue(encoder.encode(': heartbeat\n\n'));
|
||||
}, 30000);
|
||||
|
||||
// Cleanup on close
|
||||
return () => {
|
||||
clearInterval(heartbeat);
|
||||
eventBus.off(`user:${userId}`, handleEvent);
|
||||
eventBus.unsubscribeUser(userId);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
}
|
||||
184
src/lib/modules/registry.ts
Normal file
184
src/lib/modules/registry.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Module registry implementation
|
||||
* Manages loading and access to modular features
|
||||
*/
|
||||
|
||||
import type {
|
||||
Module,
|
||||
ModuleRegistry,
|
||||
AppContext,
|
||||
RouteHandler,
|
||||
Middleware,
|
||||
DatabaseAdapter,
|
||||
EventEmitter
|
||||
} from './types';
|
||||
// Module registry for extensibility
|
||||
|
||||
/**
|
||||
* Simple event emitter implementation
|
||||
*/
|
||||
class SimpleEventEmitter implements EventEmitter {
|
||||
private events: Map<string, Set<Function>> = new Map();
|
||||
|
||||
on(event: string, handler: (...args: any[]) => void): void {
|
||||
if (!this.events.has(event)) {
|
||||
this.events.set(event, new Set());
|
||||
}
|
||||
this.events.get(event)!.add(handler);
|
||||
}
|
||||
|
||||
off(event: string, handler: (...args: any[]) => void): void {
|
||||
this.events.get(event)?.delete(handler);
|
||||
}
|
||||
|
||||
emit(event: string, ...args: any[]): void {
|
||||
this.events.get(event)?.forEach(handler => {
|
||||
try {
|
||||
handler(...args);
|
||||
} catch (error) {
|
||||
console.error(`Error in event handler for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Module manager class
|
||||
*/
|
||||
export class ModuleManager {
|
||||
private modules: Map<string, Module> = new Map();
|
||||
private routes: Map<string, RouteHandler> = new Map();
|
||||
private middlewares: Middleware[] = [];
|
||||
private events = new SimpleEventEmitter();
|
||||
private initialized = false;
|
||||
|
||||
/**
|
||||
* Get app context for modules
|
||||
*/
|
||||
private getAppContext(): AppContext {
|
||||
return {
|
||||
addRoute: (path, handler) => this.addRoute(path, handler),
|
||||
addMiddleware: (middleware) => this.middlewares.push(middleware),
|
||||
db: this.getDatabaseAdapter(),
|
||||
events: this.events,
|
||||
modules: this.getRegistry(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database adapter based on deployment mode
|
||||
*/
|
||||
private getDatabaseAdapter(): DatabaseAdapter {
|
||||
// This would be implemented to use SQLite or PostgreSQL
|
||||
// based on deployment mode
|
||||
return {
|
||||
query: async (sql, params) => [],
|
||||
execute: async (sql, params) => {},
|
||||
transaction: async (fn) => fn(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a module
|
||||
*/
|
||||
async register(module: Module): Promise<void> {
|
||||
if (this.modules.has(module.name)) {
|
||||
console.warn(`Module ${module.name} is already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await module.init(this.getAppContext());
|
||||
this.modules.set(module.name, module);
|
||||
console.log(`Module ${module.name} registered successfully`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register module ${module.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a module
|
||||
*/
|
||||
async unregister(moduleName: string): Promise<void> {
|
||||
const module = this.modules.get(moduleName);
|
||||
if (!module) return;
|
||||
|
||||
if (module.cleanup) {
|
||||
await module.cleanup();
|
||||
}
|
||||
|
||||
this.modules.delete(moduleName);
|
||||
// Remove routes registered by this module
|
||||
// This would need to track which module registered which routes
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a route handler
|
||||
*/
|
||||
private addRoute(path: string, handler: RouteHandler): void {
|
||||
this.routes.set(path, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route handler for a path
|
||||
*/
|
||||
getRouteHandler(path: string): RouteHandler | null {
|
||||
return this.routes.get(path) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all middleware
|
||||
*/
|
||||
getMiddleware(): Middleware[] {
|
||||
return [...this.middlewares];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get module registry
|
||||
*/
|
||||
getRegistry(): ModuleRegistry {
|
||||
const registry: ModuleRegistry = {};
|
||||
|
||||
// Copy all modules to registry
|
||||
for (const [name, module] of this.modules) {
|
||||
registry[name] = module;
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a specific module
|
||||
*/
|
||||
get<K extends keyof ModuleRegistry>(name: K): ModuleRegistry[K] | null {
|
||||
return this.getRegistry()[name] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a module is loaded
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
return this.modules.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all modules
|
||||
*/
|
||||
emit(event: string, ...args: any[]): void {
|
||||
this.events.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
// Global module manager instance
|
||||
export const modules = new ModuleManager();
|
||||
|
||||
|
||||
// Initialize modules on app start
|
||||
export async function initializeModules() {
|
||||
// Load core modules here if any
|
||||
|
||||
// Emit initialization complete event
|
||||
modules.emit('modules:initialized');
|
||||
}
|
||||
86
src/lib/modules/types.d.ts
vendored
Normal file
86
src/lib/modules/types.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Module system type definitions
|
||||
* These interfaces allow for extensibility and plugins
|
||||
*/
|
||||
import type { APIContext } from 'astro';
|
||||
import type { ComponentType, LazyExoticComponent } from 'react';
|
||||
/**
|
||||
* Base module interface that all modules must implement
|
||||
*/
|
||||
export interface Module {
|
||||
/** Unique module identifier */
|
||||
name: string;
|
||||
/** Module version */
|
||||
version: string;
|
||||
/** Initialize the module with app context */
|
||||
init(app: AppContext): Promise<void>;
|
||||
/** Cleanup when module is unloaded */
|
||||
cleanup?(): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* Application context passed to modules
|
||||
*/
|
||||
export interface AppContext {
|
||||
/** Register API routes */
|
||||
addRoute(path: string, handler: RouteHandler): void;
|
||||
/** Register middleware */
|
||||
addMiddleware(middleware: Middleware): void;
|
||||
/** Access to database (abstracted) */
|
||||
db: DatabaseAdapter;
|
||||
/** Event emitter for cross-module communication */
|
||||
events: EventEmitter;
|
||||
/** Access to other modules */
|
||||
modules: ModuleRegistry;
|
||||
}
|
||||
/**
|
||||
* Route handler type
|
||||
*/
|
||||
export type RouteHandler = (context: APIContext) => Promise<Response> | Response;
|
||||
/**
|
||||
* Middleware type
|
||||
*/
|
||||
export type Middleware = (context: APIContext, next: () => Promise<Response>) => Promise<Response>;
|
||||
/**
|
||||
* Database adapter interface (abstract away implementation)
|
||||
*/
|
||||
export interface DatabaseAdapter {
|
||||
query<T>(sql: string, params?: any[]): Promise<T[]>;
|
||||
execute(sql: string, params?: any[]): Promise<void>;
|
||||
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
}
|
||||
/**
|
||||
* Event emitter for cross-module communication
|
||||
*/
|
||||
export interface EventEmitter {
|
||||
on(event: string, handler: (...args: any[]) => void): void;
|
||||
off(event: string, handler: (...args: any[]) => void): void;
|
||||
emit(event: string, ...args: any[]): void;
|
||||
}
|
||||
/**
|
||||
* Example module interfaces
|
||||
* These are examples of how modules can be structured
|
||||
*/
|
||||
export interface FeatureModule extends Module {
|
||||
/** React components provided by the module */
|
||||
components?: Record<string, LazyExoticComponent<ComponentType<any>>>;
|
||||
/** API methods provided by the module */
|
||||
api?: Record<string, (...args: any[]) => Promise<any>>;
|
||||
/** Lifecycle hooks */
|
||||
hooks?: {
|
||||
onInit?: () => Promise<void>;
|
||||
onUserAction?: (action: string, data: any) => Promise<void>;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Module registry interface
|
||||
*/
|
||||
export interface ModuleRegistry {
|
||||
[key: string]: Module | undefined;
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
1
src/lib/modules/types.d.ts.map
Normal file
1
src/lib/modules/types.d.ts.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
|
||||
5
src/lib/modules/types.js
Normal file
5
src/lib/modules/types.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Module system type definitions
|
||||
* These interfaces allow for extensibility and plugins
|
||||
*/
|
||||
export {};
|
||||
110
src/lib/modules/types.ts
Normal file
110
src/lib/modules/types.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Module system type definitions
|
||||
* These interfaces allow for extensibility and plugins
|
||||
*/
|
||||
|
||||
import type { APIContext } from 'astro';
|
||||
import type { ComponentType, LazyExoticComponent } from 'react';
|
||||
|
||||
/**
|
||||
* Base module interface that all modules must implement
|
||||
*/
|
||||
export interface Module {
|
||||
/** Unique module identifier */
|
||||
name: string;
|
||||
|
||||
/** Module version */
|
||||
version: string;
|
||||
|
||||
/** Initialize the module with app context */
|
||||
init(app: AppContext): Promise<void>;
|
||||
|
||||
/** Cleanup when module is unloaded */
|
||||
cleanup?(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Application context passed to modules
|
||||
*/
|
||||
export interface AppContext {
|
||||
/** Register API routes */
|
||||
addRoute(path: string, handler: RouteHandler): void;
|
||||
|
||||
/** Register middleware */
|
||||
addMiddleware(middleware: Middleware): void;
|
||||
|
||||
/** Access to database (abstracted) */
|
||||
db: DatabaseAdapter;
|
||||
|
||||
/** Event emitter for cross-module communication */
|
||||
events: EventEmitter;
|
||||
|
||||
/** Access to other modules */
|
||||
modules: ModuleRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route handler type
|
||||
*/
|
||||
export type RouteHandler = (context: APIContext) => Promise<Response> | Response;
|
||||
|
||||
/**
|
||||
* Middleware type
|
||||
*/
|
||||
export type Middleware = (
|
||||
context: APIContext,
|
||||
next: () => Promise<Response>
|
||||
) => Promise<Response>;
|
||||
|
||||
/**
|
||||
* Database adapter interface (abstract away implementation)
|
||||
*/
|
||||
export interface DatabaseAdapter {
|
||||
query<T>(sql: string, params?: any[]): Promise<T[]>;
|
||||
execute(sql: string, params?: any[]): Promise<void>;
|
||||
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event emitter for cross-module communication
|
||||
*/
|
||||
export interface EventEmitter {
|
||||
on(event: string, handler: (...args: any[]) => void): void;
|
||||
off(event: string, handler: (...args: any[]) => void): void;
|
||||
emit(event: string, ...args: any[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example module interfaces
|
||||
* These are examples of how modules can be structured
|
||||
*/
|
||||
|
||||
// Example: Feature module with components
|
||||
export interface FeatureModule extends Module {
|
||||
/** React components provided by the module */
|
||||
components?: Record<string, LazyExoticComponent<ComponentType<any>>>;
|
||||
|
||||
/** API methods provided by the module */
|
||||
api?: Record<string, (...args: any[]) => Promise<any>>;
|
||||
|
||||
/** Lifecycle hooks */
|
||||
hooks?: {
|
||||
onInit?: () => Promise<void>;
|
||||
onUserAction?: (action: string, data: any) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Module registry interface
|
||||
*/
|
||||
export interface ModuleRegistry {
|
||||
[key: string]: Module | undefined;
|
||||
}
|
||||
|
||||
// Generic types that modules might use
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
username?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user