diff --git a/.env.example b/.env.example index 66a256f..ff9eaac 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,15 @@ NEXTAUTH_SECRET=your_secret_here # API Configuration API_BASE_URL=http://localhost:3000/api +# Cryptomus API Configuration +# Get these from your Cryptomus merchant dashboard: https://cryptomus.com/merchant +CRYPTOMUS_MERCHANT_ID=your_merchant_id_here +CRYPTOMUS_PAYMENT_API_KEY=your_payment_api_key_here + # Sync Configuration -SYNC_ENABLED=true -SYNC_INTERVAL=3600000 # 1 hour in milliseconds +# Enable sync operations only from the server itself (set to 'true' on production server) +SYNC_SERVER_ONLY=false +# Automatic sync frequency in days (set to 0 to disable automatic sync) +AUTO_SYNC_DAYS=1 +# Secret key to authorize sync operations from server +SYNC_SECRET_KEY=your_sync_secret_key_here diff --git a/.env.local.example b/.env.local.example deleted file mode 100644 index d475328..0000000 --- a/.env.local.example +++ /dev/null @@ -1,7 +0,0 @@ -# Cryptomus API Configuration -# Get these from your Cryptomus merchant dashboard: https://cryptomus.com/merchant -CRYPTOMUS_MERCHANT_ID=your_merchant_id_here -CRYPTOMUS_PAYMENT_API_KEY=your_payment_api_key_here - -# NextAuth URL Configuration -NEXTAUTH_URL=http://localhost:3002 diff --git a/docs/SYNC_SECURITY.md b/docs/SYNC_SECURITY.md new file mode 100644 index 0000000..58e4d4d --- /dev/null +++ b/docs/SYNC_SECURITY.md @@ -0,0 +1,211 @@ +# RepoHub Sync Security Configuration + +Bu doküman, RepoHub sync işlemlerinin güvenliği ve otomatikleştirilmesi için yapılandırmayı açıklar. + +## Güvenlik Yapılandırması + +### Environment Değişkenleri + +`.env.local` dosyasına aşağı değişkenleri ekleyin: + +```bash +# Sync Configuration +# Enable sync operations only from the server itself (set to 'true' on production server) +SYNC_SERVER_ONLY=true +# Automatic sync frequency in days (set to 0 to disable automatic sync) +AUTO_SYNC_DAYS=1 +# Secret key to authorize sync operations from server +SYNC_SECRET_KEY=your_very_secure_secret_key_here +``` + +### Değişkenlerin Açıklaması + +- **SYNC_SERVER_ONLY**: `true` olarak ayarlandığında, sync işlemleri sadece sunucudan yapılabilir +- **AUTO_SYNC_DAYS**: Otomatik sync sıklığı (gün olarak). `0` = otomatik sync kapalı +- **SYNC_SECRET_KEY**: Sunucu sync işlemleri için gerekli gizli anahtar + +## Sunucu Sync İşlemleri + +### 1. Manuel Sync (Sunucudan) + +Sunucuda sync işlemleri yapmak için: + +```bash +# Tüm platformları sync et +./scripts/server-sync.sh auto-sync + +# Sadece Windows paketlerini sync et +./scripts/server-sync.sh sync-winget + +# Sync durumunu kontrol et +./scripts/server-sync.sh status +``` + +### 2. Cron Job ile Otomatik Sync + +Otomatik sync kurulumu için: + +```bash +# Günlük otomatik sync kur +sudo ./scripts/cron-setup.sh install + +# Haftalık otomatik sync kur (her 7 günde bir) +sudo AUTO_SYNC_DAYS=7 ./scripts/cron-setup.sh install + +# Cron job durumunu kontrol et +sudo ./scripts/cron-setup.sh status + +# Cron job'ı kaldır +sudo ./scripts/cron-setup.sh remove +``` + +## API Güvenliği + +### Sync Endpoint'leri + +Tüm sync endpoint'leri artık güvenlik kontrolü yapar: + +- `/api/sync` - Genel sync endpoint'i +- `/api/sync-winget` - Windows paket sync'i +- `/api/sync-homebrew` - macOS paket sync'i +- `/api/sync-fedora` - Fedora paket sync'i +- `/api/sync-arch` - Arch paket sync'i +- `/api/auto-sync` - Otomatik sync + +### Güvenlik Kontrolü + +**Server-only modda (`SYNC_SERVER_ONLY=true`):** + +1. **Secret Key Kontrolü**: Request header'ında `x-sync-secret` olmalı +2. **IP Kontrolü**: İstek localhost'tan gelmeli veya doğru secret key içermeli + +**Normal modda (`SYNC_SERVER_ONLY=false`):** + +- Herkes sync işlemi yapabilir (geliştirme için) + +### Örnek API Kullanımı + +```bash +# Server-only modda sync yapmak +curl -X POST \ + -H "Content-Type: application/json" \ + -H "x-sync-secret: your_secret_key" \ + -d '{}' \ + http://localhost:3002/api/sync-winget + +# Sync durumunu kontrol et +curl -H "x-sync-secret: your_secret_key" \ + http://localhost:3002/api/sync +``` + +## Güvenlik İpuçları + +### 1. Secret Key Güvenliği + +- Güçlü ve rastgele bir secret key kullanın +- Secret key'i `.env.local` dosyasında saklayın, asla kod içine koymayın +- Secret key'i düzenli olarak değiştirin + +### 2. Sunucu Güvenliği + +- Sync script'lerini sadece sunucuda çalıştırın +- Cron job'ları root kullanıcısı olarak ayarlayın +- Log dosyalarını düzenli olarak kontrol edin + +### 3. Ağ Güvenliği + +- Sync endpoint'lerini firewall ile koruyun +- Sadece localhost'tan erişime izin verin +- SSL/TLS kullanın (production'da) + +## Monitoring ve Logging + +### Log Dosyaları + +- **Sync Log**: `/var/log/repohub-sync.log` +- **Cron Log**: `/var/log/cron.log` (sistem bağımlı) + +### Log İzleme + +```bash +# Son 20 sync log satırını göster +tail -20 /var/log/repohub-sync.log + +# Real-time log izleme +tail -f /var/log/repohub-sync.log + +# Cron job loglarını kontrol et +sudo tail -20 /var/log/cron.log +``` + +## Troubleshooting + +### Yaygın Sorunlar + +1. **403 Forbidden Error** + - `SYNC_SECRET_KEY` doğru ayarlanmamış + - Header'da `x-sync-secret` eksik + +2. **Cron Job Çalışmıyor** + - Script executable değil + - Environment değişkenleri eksik + - Log dosyası izinleri yanlış + +3. **Sync Başarısız** + - API URL yanlış + - Ağ bağlantısı sorunu + - Disk alanı yetersiz + +### Debug Komutları + +```bash +# Sync script test et +./scripts/server-sync.sh test + +# Cron job durumunu kontrol et +sudo ./scripts/cron-setup.sh status + +# Environment değişkenlerini kontrol et +env | grep SYNC +``` + +## Production Dağıtımı + +### Adım 1: Environment Ayarı + +```bash +# .env.local dosyasını production'a kopyala +cp .env.local.example .env.local +# .env.local dosyasını production değerleriyle düzenle +``` + +### Adım 2: Secret Key Oluştur + +```bash +# Güçlü secret key oluştur +openssl rand -hex 32 +# Bu değeri .env.local dosyasına ekle +``` + +### Adım 3: Cron Job Kur + +```bash +# Production cron job kur +sudo AUTO_SYNC_DAYS=1 ./scripts/cron-setup.sh install +``` + +### Adım 4: Test Et + +```bash +# Sync işlemini test et +./scripts/server-sync.sh test + +# Otomatik sync'i test et +curl -X POST \ + -H "Content-Type: application/json" \ + -H "x-sync-secret: your_secret_key" \ + -d '{}' \ + http://localhost:3002/api/auto-sync +``` + +Bu yapılandırma ile sync işlemleriniz güvenli ve otomatik hale gelecektir. diff --git a/scripts/cron-setup.sh b/scripts/cron-setup.sh new file mode 100755 index 0000000..d8dfc0b --- /dev/null +++ b/scripts/cron-setup.sh @@ -0,0 +1,199 @@ +#!/bin/bash + +# Cron job setup script for RepoHub auto-sync +# This script sets up automatic sync using cron jobs + +set -e + +# Configuration +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SYNC_SCRIPT="$SCRIPT_DIR/server-sync.sh" +CRON_FILE="/etc/cron.d/repohub-sync" +LOG_FILE="/var/log/repohub-sync.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check if running as root +check_root() { + if [ "$EUID" -ne 0 ]; then + log_error "This script must be run as root to setup cron jobs" + echo "Please run: sudo $0" + exit 1 + fi +} + +# Check if sync script exists +check_sync_script() { + if [ ! -f "$SYNC_SCRIPT" ]; then + log_error "Sync script not found: $SYNC_SCRIPT" + exit 1 + fi + + # Make sure sync script is executable + chmod +x "$SYNC_SCRIPT" + log_info "Sync script is ready: $SYNC_SCRIPT" +} + +# Get sync frequency from environment or default to daily +get_sync_frequency() { + local frequency="${AUTO_SYNC_DAYS:-1}" + echo "$frequency" +} + +# Setup cron job +setup_cron() { + local frequency=$(get_sync_frequency) + local cron_schedule="0 2 */$frequency * *" # Run at 2 AM every N days + + log_info "Setting up cron job to run every $frequency day(s)" + log_info "Cron schedule: $cron_schedule" + + # Create cron file + cat > "$CRON_FILE" << EOF +# RepoHub Auto Sync - Runs every $frequency day(s) at 2 AM +# Environment variables +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin +API_URL=${API_URL:-http://localhost:3002} +SYNC_SECRET_KEY=$SYNC_SECRET_KEY +AUTO_SYNC_DAYS=$frequency + +# Cron job +$cron_schedule root $SYNC_SCRIPT auto-sync >> $LOG_FILE 2>&1 + +# Manual sync status check (every hour) +0 * * * * root $SYNC_SCRIPT status >> $LOG_FILE 2>&1 +EOF + + # Set proper permissions + chmod 644 "$CRON_FILE" + + # Reload cron service + if command -v systemctl >/dev/null 2>&1; then + systemctl reload cron || systemctl reload crond || true + elif command -v service >/dev/null 2>&1; then + service cron reload || service crond reload || true + fi + + log_info "Cron job installed successfully: $CRON_FILE" +} + +# Remove cron job +remove_cron() { + if [ -f "$CRON_FILE" ]; then + rm -f "$CRON_FILE" + log_info "Cron job removed: $CRON_FILE" + + # Reload cron service + if command -v systemctl >/dev/null 2>&1; then + systemctl reload cron || systemctl reload crond || true + elif command -v service >/dev/null 2>&1; then + service cron reload || service crond reload || true + fi + else + log_warn "No cron job found to remove" + fi +} + +# Show cron status +show_status() { + if [ -f "$CRON_FILE" ]; then + log_info "Cron job is installed:" + cat "$CRON_FILE" + else + log_warn "No cron job found" + fi + + if [ -f "$LOG_FILE" ]; then + log_info "Recent sync logs:" + tail -20 "$LOG_FILE" + else + log_warn "No log file found: $LOG_FILE" + fi +} + +# Test sync script +test_sync() { + log_info "Testing sync script..." + + if [ -z "$SYNC_SECRET_KEY" ]; then + log_error "SYNC_SECRET_KEY environment variable is required for testing" + exit 1 + fi + + # Run a status check to test connectivity + "$SYNC_SCRIPT" status +} + +# Show usage +show_usage() { + echo "Usage: $0 [COMMAND]" + echo "" + echo "Commands:" + echo " install Install cron job for auto-sync" + echo " remove Remove cron job" + echo " status Show cron job status and recent logs" + echo " test Test sync script connectivity" + echo " help Show this help message" + echo "" + echo "Environment variables:" + echo " API_URL API base URL (default: http://localhost:3002)" + echo " SYNC_SECRET_KEY Secret key for sync authorization" + echo " AUTO_SYNC_DAYS Sync frequency in days (default: 1)" + echo " LOG_FILE Log file path (default: /var/log/repohub-sync.log)" + echo "" + echo "Examples:" + echo " $0 install # Install daily cron job" + echo " AUTO_SYNC_DAYS=7 $0 install # Install weekly cron job" + echo " $0 status # Check cron job status" + echo " $0 test # Test sync script" +} + +# Main script logic +main() { + case "${1:-}" in + "install") + check_root + check_sync_script + setup_cron + ;; + "remove") + check_root + remove_cron + ;; + "status") + show_status + ;; + "test") + check_sync_script + test_sync + ;; + "help"|"-h"|"--help") + show_usage + ;; + *) + log_error "Unknown command: ${1:-}" + show_usage + exit 1 + ;; + esac +} + +# Run main function +main "$@" diff --git a/scripts/server-sync.sh b/scripts/server-sync.sh new file mode 100755 index 0000000..2a8d4cf --- /dev/null +++ b/scripts/server-sync.sh @@ -0,0 +1,185 @@ +#!/bin/bash + +# Server-side sync script for RepoHub +# This script can only be run from the server itself when SYNC_SERVER_ONLY=true + +set -e + +# Configuration +API_URL="${API_URL:-http://localhost:3002}" +SYNC_SECRET="${SYNC_SECRET_KEY}" +LOG_FILE="/var/log/repohub-sync.log" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging function +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" +} + +log_info() { + echo -e "${GREEN}$(date '+%Y-%m-%d %H:%M:%S') - INFO: $1${NC}" | tee -a "$LOG_FILE" +} + +log_warn() { + echo -e "${YELLOW}$(date '+%Y-%m-%d %H:%M:%S') - WARN: $1${NC}" | tee -a "$LOG_FILE" +} + +log_error() { + echo -e "${RED}$(date '+%Y-%m-%d %H:%M:%S') - ERROR: $1${NC}" | tee -a "$LOG_FILE" +} + +# Check if required environment variables are set +check_env() { + if [ -z "$SYNC_SECRET" ]; then + log_error "SYNC_SECRET_KEY environment variable is required" + exit 1 + fi + + log_info "Environment variables validated" +} + +# Perform sync for a specific platform +sync_platform() { + local platform="$1" + log_info "Starting sync for platform: $platform" + + response=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "x-sync-secret: $SYNC_SECRET" \ + -d '{}' \ + "$API_URL/api/sync-$platform") + + http_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | head -n -1) + + if [ "$http_code" -eq 200 ]; then + log_info "Successfully synced $platform: $body" + return 0 + else + log_error "Failed to sync $platform (HTTP $http_code): $body" + return 1 + fi +} + +# Perform auto sync +auto_sync() { + log_info "Starting automatic sync for all platforms" + + response=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "x-sync-secret: $SYNC_SECRET" \ + -d '{}' \ + "$API_URL/api/auto-sync") + + http_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | head -n -1) + + if [ "$http_code" -eq 200 ]; then + log_info "Auto sync completed successfully: $body" + return 0 + else + log_error "Auto sync failed (HTTP $http_code): $body" + return 1 + fi +} + +# Get sync status +get_status() { + log_info "Getting sync status" + + response=$(curl -s -w "\n%{http_code}" -X GET \ + -H "x-sync-secret: $SYNC_SECRET" \ + "$API_URL/api/sync") + + http_code=$(echo "$response" | tail -n1) + body=$(echo "$response" | head -n -1) + + if [ "$http_code" -eq 200 ]; then + log_info "Sync status: $body" + return 0 + else + log_error "Failed to get sync status (HTTP $http_code): $body" + return 1 + fi +} + +# Show usage +show_usage() { + echo "Usage: $0 [COMMAND] [OPTIONS]" + echo "" + echo "Commands:" + echo " auto-sync Perform automatic sync for all platforms" + echo " sync-all Sync all platforms individually" + echo " sync-winget Sync Windows packages" + echo " sync-homebrew Sync macOS packages" + echo " sync-fedora Sync Fedora packages" + echo " sync-arch Sync Arch Linux packages" + echo " status Get current sync status" + echo "" + echo "Environment variables:" + echo " API_URL API base URL (default: http://localhost:3002)" + echo " SYNC_SECRET_KEY Secret key for sync authorization" + echo " LOG_FILE Log file path (default: /var/log/repohub-sync.log)" + echo "" + echo "Examples:" + echo " $0 auto-sync # Auto sync all platforms" + echo " $0 sync-winget # Sync only Windows packages" + echo " API_URL=https://api.repohub.com $0 status # Check status on production" +} + +# Main script logic +main() { + # Create log file if it doesn't exist + mkdir -p "$(dirname "$LOG_FILE")" + touch "$LOG_FILE" + + log_info "RepoHub Server Sync Script started" + + check_env + + case "${1:-}" in + "auto-sync") + auto_sync + ;; + "sync-all") + log_info "Syncing all platforms individually" + sync_platform "winget" + sync_platform "homebrew" + sync_platform "fedora" + sync_platform "arch" + ;; + "sync-winget") + sync_platform "winget" + ;; + "sync-homebrew") + sync_platform "homebrew" + ;; + "sync-fedora") + sync_platform "fedora" + ;; + "sync-arch") + sync_platform "arch" + ;; + "status") + get_status + ;; + "help"|"-h"|"--help") + show_usage + ;; + *) + log_error "Unknown command: ${1:-}" + show_usage + exit 1 + ;; + esac + + log_info "RepoHub Server Sync Script completed" +} + +# Run main function with all arguments +main "$@" diff --git a/src/app/api/auto-sync/route.ts b/src/app/api/auto-sync/route.ts new file mode 100644 index 0000000..614d297 --- /dev/null +++ b/src/app/api/auto-sync/route.ts @@ -0,0 +1,148 @@ +import { NextRequest, NextResponse } from 'next/server' +import { SyncAuth } from '@/lib/sync/auth' +import { MetadataFetcher } from '@/services/metadataFetcher' +import { PackageFetcherV2 } from '@/services/packageFetcherV2' +import { WingetPackageFetcher } from '@/services/wingetPackageFetcher' +import { HomebrewPackageFetcher } from '@/services/homebrewPackageFetcher' +import { FedoraPackageFetcher } from '@/services/fedoraPackageFetcher' +import { ArchPackageFetcher } from '@/services/archPackageFetcher' + +export const dynamic = 'force-dynamic' +export const maxDuration = 1800 // 30 minutes timeout for auto sync + +// Simple in-memory store for last sync time (in production, use database) +let lastAutoSync: Date | null = null + +export async function POST(request: NextRequest) { + try { + // Check if auto sync is enabled + if (!SyncAuth.isAutoSyncEnabled()) { + return NextResponse.json({ + message: 'Auto sync is disabled', + next_sync: null + }) + } + + // Check if enough time has passed since last sync + const now = new Date() + const nextSyncTime = SyncAuth.getNextSyncTime(lastAutoSync || undefined) + + if (now < nextSyncTime) { + return NextResponse.json({ + message: 'Auto sync not due yet', + last_sync: lastAutoSync?.toISOString(), + next_sync: nextSyncTime.toISOString(), + hours_until_next: Math.ceil((nextSyncTime.getTime() - now.getTime()) / (1000 * 60 * 60)) + }) + } + + console.log('🔄 Starting automatic package sync...') + + // Sync all platforms in sequence + const syncResults = [] + + try { + // Sync Debian/Ubuntu packages + console.log('Syncing Debian/Ubuntu packages...') + await PackageFetcherV2.syncAll() + syncResults.push({ platform: 'debian/ubuntu', status: 'success' }) + } catch (error) { + console.error('Debian/Ubuntu sync failed:', error) + syncResults.push({ platform: 'debian/ubuntu', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error' }) + } + + try { + // Sync Windows packages (Winget) + console.log('Syncing Windows packages...') + const wingetFetcher = new WingetPackageFetcher() + const wingetPackages = await wingetFetcher.fetchAllPackages() + await wingetFetcher.storePackages(wingetPackages) + syncResults.push({ platform: 'windows', status: 'success', package_count: wingetPackages.length }) + } catch (error) { + console.error('Windows sync failed:', error) + syncResults.push({ platform: 'windows', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error' }) + } + + try { + // Sync macOS packages (Homebrew) + console.log('Syncing macOS packages...') + const homebrewFetcher = new HomebrewPackageFetcher() + const homebrewPackages = await homebrewFetcher.fetchAllPackages() + await homebrewFetcher.storePackages(homebrewPackages) + syncResults.push({ platform: 'macos', status: 'success', package_count: homebrewPackages.length }) + } catch (error) { + console.error('macOS sync failed:', error) + syncResults.push({ platform: 'macos', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error' }) + } + + try { + // Sync Fedora packages + console.log('Syncing Fedora packages...') + const fedoraFetcher = new FedoraPackageFetcher() + const fedoraPackages = await fedoraFetcher.fetchAllPackages() + await fedoraFetcher.storePackages(fedoraPackages) + syncResults.push({ platform: 'fedora', status: 'success', package_count: fedoraPackages.length }) + } catch (error) { + console.error('Fedora sync failed:', error) + syncResults.push({ platform: 'fedora', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error' }) + } + + try { + // Sync Arch packages + console.log('Syncing Arch packages...') + const archFetcher = new ArchPackageFetcher() + const archPackages = await archFetcher.fetchAllPackages() + await archFetcher.storePackages(archPackages) + syncResults.push({ platform: 'arch', status: 'success', package_count: archPackages.length }) + } catch (error) { + console.error('Arch sync failed:', error) + syncResults.push({ platform: 'arch', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error' }) + } + + // Update last sync time + lastAutoSync = now + const nextSync = SyncAuth.getNextSyncTime(lastAutoSync) + + const successCount = syncResults.filter(r => r.status === 'success').length + const totalCount = syncResults.length + + console.log(`✅ Auto sync completed: ${successCount}/${totalCount} platforms synced successfully`) + + return NextResponse.json({ + message: 'Auto sync completed', + timestamp: now.toISOString(), + last_sync: lastAutoSync.toISOString(), + next_sync: nextSync.toISOString(), + results: syncResults, + summary: { + total_platforms: totalCount, + successful: successCount, + failed: totalCount - successCount + } + }) + + } catch (error) { + console.error('Auto sync failed:', error) + return NextResponse.json( + { + error: 'Auto sync failed', + details: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString() + }, + { status: 500 } + ) + } +} + +export async function GET() { + const now = new Date() + const nextSyncTime = SyncAuth.getNextSyncTime(lastAutoSync || undefined) + + return NextResponse.json({ + auto_sync_enabled: SyncAuth.isAutoSyncEnabled(), + auto_sync_days: SyncAuth.getAutoSyncDays(), + last_sync: lastAutoSync?.toISOString(), + next_sync: SyncAuth.isAutoSyncEnabled() ? nextSyncTime.toISOString() : null, + status: lastAutoSync && now < nextSyncTime ? 'waiting' : 'ready' + }) +} diff --git a/src/app/api/sync-arch/route.ts b/src/app/api/sync-arch/route.ts index 292afc3..ae4065d 100644 --- a/src/app/api/sync-arch/route.ts +++ b/src/app/api/sync-arch/route.ts @@ -1,5 +1,6 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { ArchPackageFetcher } from '@/services/archPackageFetcher' +import { SyncAuth } from '@/lib/sync/auth' export const dynamic = 'force-dynamic' export const maxDuration = 300 // 5 minutes timeout @@ -19,7 +20,16 @@ export async function GET() { return NextResponse.json(syncStatus) } -export async function POST() { +export async function POST(request: NextRequest) { + // Check if sync is allowed + const authResult = await SyncAuth.isSyncAllowed(request) + if (!authResult.allowed) { + return NextResponse.json( + { error: 'Sync operation not allowed', reason: authResult.reason }, + { status: 403 } + ) + } + if (syncInProgress) { return NextResponse.json( { error: 'Sync already in progress' }, diff --git a/src/app/api/sync-fedora/route.ts b/src/app/api/sync-fedora/route.ts index ec9bacf..c18d918 100644 --- a/src/app/api/sync-fedora/route.ts +++ b/src/app/api/sync-fedora/route.ts @@ -1,5 +1,6 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { FedoraPackageFetcher } from '@/services/fedoraPackageFetcher' +import { SyncAuth } from '@/lib/sync/auth' export const dynamic = 'force-dynamic' export const maxDuration = 300 // 5 minutes timeout @@ -19,7 +20,16 @@ export async function GET() { return NextResponse.json(syncStatus) } -export async function POST() { +export async function POST(request: NextRequest) { + // Check if sync is allowed + const authResult = await SyncAuth.isSyncAllowed(request) + if (!authResult.allowed) { + return NextResponse.json( + { error: 'Sync operation not allowed', reason: authResult.reason }, + { status: 403 } + ) + } + if (syncInProgress) { return NextResponse.json( { error: 'Sync already in progress' }, diff --git a/src/app/api/sync-homebrew/route.ts b/src/app/api/sync-homebrew/route.ts index f28abbc..c808af6 100644 --- a/src/app/api/sync-homebrew/route.ts +++ b/src/app/api/sync-homebrew/route.ts @@ -1,5 +1,6 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { HomebrewPackageFetcher } from '@/services/homebrewPackageFetcher' +import { SyncAuth } from '@/lib/sync/auth' export const dynamic = 'force-dynamic' export const maxDuration = 300 // 5 minutes timeout @@ -19,7 +20,16 @@ export async function GET() { return NextResponse.json(syncStatus) } -export async function POST() { +export async function POST(request: NextRequest) { + // Check if sync is allowed + const authResult = await SyncAuth.isSyncAllowed(request) + if (!authResult.allowed) { + return NextResponse.json( + { error: 'Sync operation not allowed', reason: authResult.reason }, + { status: 403 } + ) + } + if (syncInProgress) { return NextResponse.json( { error: 'Sync already in progress' }, diff --git a/src/app/api/sync-winget/route.ts b/src/app/api/sync-winget/route.ts index b2f7350..6bbf6db 100644 --- a/src/app/api/sync-winget/route.ts +++ b/src/app/api/sync-winget/route.ts @@ -1,5 +1,6 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { WingetPackageFetcher } from '@/services/wingetPackageFetcher' +import { SyncAuth } from '@/lib/sync/auth' export const dynamic = 'force-dynamic' export const maxDuration = 300 // 5 minutes timeout @@ -19,7 +20,16 @@ export async function GET() { return NextResponse.json(syncStatus) } -export async function POST() { +export async function POST(request: NextRequest) { + // Check if sync is allowed + const authResult = await SyncAuth.isSyncAllowed(request) + if (!authResult.allowed) { + return NextResponse.json( + { error: 'Sync operation not allowed', reason: authResult.reason }, + { status: 403 } + ) + } + if (syncInProgress) { return NextResponse.json( { error: 'Sync already in progress' }, diff --git a/src/app/api/sync/route.ts b/src/app/api/sync/route.ts index f5cb855..25dc710 100644 --- a/src/app/api/sync/route.ts +++ b/src/app/api/sync/route.ts @@ -4,8 +4,17 @@ import { DebianPackageFetcher } from '@/services/debianPackageFetcher' import { PackageFetcherV2 } from '@/services/packageFetcherV2' import { SimplePackageFetcher } from '@/services/simplePackageFetcher' import { PlatformInitializer } from '@/services/platformInitializer' +import { SyncAuth } from '@/lib/sync/auth' export async function POST(request: NextRequest) { + // Check if sync is allowed + const authResult = await SyncAuth.isSyncAllowed(request) + if (!authResult.allowed) { + return NextResponse.json( + { error: 'Sync operation not allowed', reason: authResult.reason }, + { status: 403 } + ) + } try { const body = await request.json() const { platform_id, all_platforms, source } = body @@ -107,11 +116,16 @@ export async function POST(request: NextRequest) { export async function GET() { try { - // Return sync status (would need to implement status tracking) + // Return sync status with configuration info return NextResponse.json({ status: 'ready', last_sync: null, - platforms: ['ubuntu', 'fedora', 'arch', 'windows', 'macos'] + platforms: ['ubuntu', 'fedora', 'arch', 'windows', 'macos'], + sync_config: { + server_only: process.env.SYNC_SERVER_ONLY === 'true', + auto_sync_enabled: SyncAuth.isAutoSyncEnabled(), + auto_sync_days: SyncAuth.getAutoSyncDays() + } }) } catch (error) { console.error('Error getting sync status:', error) diff --git a/src/lib/sync/auth.ts b/src/lib/sync/auth.ts new file mode 100644 index 0000000..9a57ae3 --- /dev/null +++ b/src/lib/sync/auth.ts @@ -0,0 +1,86 @@ +import { NextRequest } from 'next/server' + +export class SyncAuth { + private static readonly SERVER_ONLY = process.env.SYNC_SERVER_ONLY === 'true' + private static readonly SYNC_SECRET = process.env.SYNC_SECRET_KEY + + /** + * Check if sync operations are allowed from the current request + */ + static async isSyncAllowed(request: NextRequest): Promise<{ allowed: boolean; reason?: string }> { + // If server-only mode is disabled, allow all requests + if (!this.SERVER_ONLY) { + return { allowed: true } + } + + // In server-only mode, check for secret key in header + const secretKey = request.headers.get('x-sync-secret') + + if (!this.SYNC_SECRET) { + return { + allowed: false, + reason: 'Sync secret key not configured on server' + } + } + + if (!secretKey) { + return { + allowed: false, + reason: 'Sync secret key required in server-only mode' + } + } + + if (secretKey !== this.SYNC_SECRET) { + return { + allowed: false, + reason: 'Invalid sync secret key' + } + } + + // Additional check: verify request is from localhost or same server + const clientIP = request.headers.get('x-forwarded-for') || + request.headers.get('x-real-ip') || + 'unknown' + + const allowedIPs = ['127.0.0.1', 'localhost', '::1'] + const isLocalRequest = allowedIPs.includes(clientIP.split(',')[0].trim()) + + if (!isLocalRequest && secretKey !== this.SYNC_SECRET) { + return { + allowed: false, + reason: 'Sync operations only allowed from server in server-only mode' + } + } + + return { allowed: true } + } + + /** + * Get automatic sync frequency in days + */ + static getAutoSyncDays(): number { + const days = parseInt(process.env.AUTO_SYNC_DAYS || '1', 10) + return isNaN(days) ? 1 : Math.max(0, days) + } + + /** + * Check if automatic sync is enabled + */ + static isAutoSyncEnabled(): boolean { + return this.getAutoSyncDays() > 0 + } + + /** + * Get the next sync time based on frequency + */ + static getNextSyncTime(lastSyncTime?: Date): Date { + const days = this.getAutoSyncDays() + if (days === 0) { + return new Date(0) // Return epoch time if disabled + } + + const nextSync = new Date(lastSyncTime || new Date()) + nextSync.setDate(nextSync.getDate() + days) + return nextSync + } +}