diff --git a/package-lock.json b/package-lock.json index 6470d39..86b7c8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "input-otp": "^1.2.4", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.462.0", - "mysql2": "^3.12.0", + "mysql2": "^3.14.3", "nanoid": "^5.1.5", "next-themes": "^0.3.0", "react": "^18.3.1", diff --git a/package.json b/package.json index ea1c4b0..b51c4dd 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "input-otp": "^1.2.4", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.462.0", - "mysql2": "^3.12.0", + "mysql2": "^3.14.3", "nanoid": "^5.1.5", "next-themes": "^0.3.0", "react": "^18.3.1", diff --git a/setup-database-v2.cjs b/setup-database-v2.cjs new file mode 100644 index 0000000..c2ed02a --- /dev/null +++ b/setup-database-v2.cjs @@ -0,0 +1,490 @@ +#!/usr/bin/env node + +const mysql = require('mysql2/promise'); +const { nanoid } = require('nanoid'); + +// Database configuration +const dbConfig = { + host: '192.168.1.146', + port: 5444, + user: 'root', + password: 'j3bv5YmVN4CVwLmoMV6oVIMF62hhc8pBRaSWrIWvLIKIdZOAkNFbUa3ntKwCKABC', + database: 'scriptshare', +}; + +// SQL to create tables (individual queries) +const createTableQueries = [ + `CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(255) PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + username VARCHAR(100) NOT NULL UNIQUE, + display_name VARCHAR(100) NOT NULL, + avatar_url VARCHAR(500), + bio TEXT, + is_admin BOOLEAN DEFAULT FALSE, + is_moderator BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX email_idx (email), + INDEX username_idx (username) + )`, + + `CREATE TABLE IF NOT EXISTS scripts ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + content TEXT NOT NULL, + compatible_os JSON NOT NULL, + categories JSON NOT NULL, + tags JSON, + git_repository_url VARCHAR(500), + author_id VARCHAR(255) NOT NULL, + author_name VARCHAR(100) NOT NULL, + view_count INT DEFAULT 0, + download_count INT DEFAULT 0, + rating INT DEFAULT 0, + rating_count INT DEFAULT 0, + is_approved BOOLEAN DEFAULT FALSE, + is_public BOOLEAN DEFAULT TRUE, + version VARCHAR(20) DEFAULT '1.0.0', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX author_idx (author_id), + INDEX approved_idx (is_approved), + INDEX public_idx (is_public), + INDEX created_at_idx (created_at) + )`, + + `CREATE TABLE IF NOT EXISTS script_versions ( + id VARCHAR(255) PRIMARY KEY, + script_id VARCHAR(255) NOT NULL, + version VARCHAR(20) NOT NULL, + content TEXT NOT NULL, + changelog TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(255) NOT NULL, + INDEX script_idx (script_id), + INDEX version_idx (version) + )`, + + `CREATE TABLE IF NOT EXISTS ratings ( + id VARCHAR(255) PRIMARY KEY, + script_id VARCHAR(255) NOT NULL, + user_id VARCHAR(255) NOT NULL, + rating INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX script_idx (script_id), + INDEX user_idx (user_id), + INDEX unique_rating (script_id, user_id) + )`, + + `CREATE TABLE IF NOT EXISTS script_collections ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + author_id VARCHAR(255) NOT NULL, + is_public BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX author_idx (author_id), + INDEX public_idx (is_public) + )`, + + `CREATE TABLE IF NOT EXISTS collection_scripts ( + id VARCHAR(255) PRIMARY KEY, + collection_id VARCHAR(255) NOT NULL, + script_id VARCHAR(255) NOT NULL, + added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX collection_idx (collection_id), + INDEX script_idx (script_id) + )`, + + `CREATE TABLE IF NOT EXISTS script_analytics ( + id VARCHAR(255) PRIMARY KEY, + script_id VARCHAR(255) NOT NULL, + event_type VARCHAR(50) NOT NULL, + user_id VARCHAR(255), + user_agent TEXT, + ip_address VARCHAR(45), + referrer VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX script_idx (script_id), + INDEX event_idx (event_type), + INDEX user_idx (user_id), + INDEX created_at_idx (created_at) + )` +]; + +// Generate demo data +const generateDemoData = () => { + const users = [ + { + id: nanoid(), + email: 'admin@scriptshare.com', + username: 'admin', + display_name: 'Admin User', + avatar_url: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin', + bio: 'Platform administrator', + is_admin: true, + is_moderator: true, + }, + { + id: nanoid(), + email: 'john.doe@example.com', + username: 'johndoe', + display_name: 'John Doe', + avatar_url: 'https://api.dicebear.com/7.x/avataaars/svg?seed=john', + bio: 'Full-stack developer and automation enthusiast', + is_admin: false, + is_moderator: false, + }, + { + id: nanoid(), + email: 'jane.smith@example.com', + username: 'janesmith', + display_name: 'Jane Smith', + avatar_url: 'https://api.dicebear.com/7.x/avataaars/svg?seed=jane', + bio: 'DevOps engineer who loves scripting', + is_admin: false, + is_moderator: true, + }, + ]; + + const scripts = [ + { + id: nanoid(), + name: 'System Monitor Dashboard', + description: 'A comprehensive system monitoring script that displays CPU, memory, disk usage, and network statistics in a beautiful dashboard format.', + content: `#!/bin/bash + +# System Monitor Dashboard +# Displays real-time system statistics + +echo "=== SYSTEM MONITOR DASHBOARD ===" +echo "Generated: $(date)" +echo "===============================" + +# CPU Usage +echo "📊 CPU Usage:" +top -bn1 | grep "Cpu(s)" | awk '{print $2 $3}' | awk -F'%' '{print $1"%"}' + +# Memory Usage +echo "💾 Memory Usage:" +free -h | awk 'NR==2{printf "Used: %s/%s (%.2f%%)", $3,$2,$3*100/$2 }' + +# Disk Usage +echo "💿 Disk Usage:" +df -h | awk '$NF=="/"{printf "Used: %s/%s (%s)", $3,$2,$5}' + +# Network Stats +echo "🌐 Network Statistics:" +cat /proc/net/dev | awk 'NR>2 {print $1 $2 $10}' | head -5 + +echo "==============================="`, + compatible_os: ['linux', 'macos'], + categories: ['monitoring', 'system'], + tags: ['bash', 'system-info', 'dashboard'], + git_repository_url: 'https://github.com/example/system-monitor', + author_id: users[1].id, + author_name: users[1].display_name, + view_count: 245, + download_count: 89, + rating: 4.5, + rating_count: 12, + is_approved: true, + is_public: true, + version: '2.1.0', + }, + { + id: nanoid(), + name: 'Automated Backup Script', + description: 'Intelligent backup solution that automatically backs up specified directories to multiple destinations with compression and encryption.', + content: `#!/bin/bash + +# Automated Backup Script v1.5 +# Creates encrypted backups with rotation + +BACKUP_DIR="/path/to/backup" +SOURCE_DIRS=("/home/user/documents" "/home/user/projects") +RETENTION_DAYS=30 + +echo "🔒 Starting automated backup..." + +for dir in "\${SOURCE_DIRS[@]}"; do + if [ -d "$dir" ]; then + timestamp=$(date +"%Y%m%d_%H%M%S") + backup_name="backup_$(basename $dir)_$timestamp.tar.gz" + + echo "📦 Backing up $dir..." + tar -czf "$BACKUP_DIR/$backup_name" "$dir" + + # Encrypt backup + gpg --cipher-algo AES256 --compress-algo 1 --symmetric \\ + --output "$BACKUP_DIR/$backup_name.gpg" "$BACKUP_DIR/$backup_name" + + rm "$BACKUP_DIR/$backup_name" + echo "✅ Backup completed: $backup_name.gpg" + fi +done + +# Cleanup old backups +find "$BACKUP_DIR" -name "*.gpg" -mtime +$RETENTION_DAYS -delete + +echo "🎉 Backup process completed!"`, + compatible_os: ['linux', 'macos'], + categories: ['backup', 'automation'], + tags: ['bash', 'backup', 'encryption', 'cron'], + author_id: users[2].id, + author_name: users[2].display_name, + view_count: 156, + download_count: 67, + rating: 4.8, + rating_count: 8, + is_approved: true, + is_public: true, + version: '1.5.0', + }, + { + id: nanoid(), + name: 'Development Environment Setup', + description: 'One-click setup script for complete development environment including Node.js, Python, Docker, and essential tools.', + content: `#!/bin/bash + +# Development Environment Setup Script +# Sets up a complete development environment + +echo "🚀 Setting up development environment..." + +# Update system +echo "📦 Updating system packages..." +sudo apt update && sudo apt upgrade -y + +# Install Node.js via NVM +echo "📗 Installing Node.js..." +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash +source ~/.bashrc +nvm install --lts +nvm use --lts + +# Install Python and pip +echo "🐍 Installing Python..." +sudo apt install python3 python3-pip -y + +# Install Docker +echo "🐳 Installing Docker..." +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh +sudo usermod -aG docker $USER + +# Install VS Code +echo "💻 Installing VS Code..." +wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg +sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/ +sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list' +sudo apt update && sudo apt install code -y + +# Install essential tools +echo "🔧 Installing essential tools..." +sudo apt install git curl wget htop tree jq -y + +echo "✅ Development environment setup complete!" +echo "Please log out and back in for Docker permissions to take effect."`, + compatible_os: ['linux'], + categories: ['development', 'setup'], + tags: ['bash', 'setup', 'nodejs', 'python', 'docker'], + author_id: users[1].id, + author_name: users[1].display_name, + view_count: 89, + download_count: 34, + rating: 4.2, + rating_count: 5, + is_approved: true, + is_public: true, + version: '1.0.0', + }, + { + id: nanoid(), + name: 'Log Analyzer Pro', + description: 'Advanced log file analyzer that searches for patterns, generates reports, and alerts on suspicious activities.', + content: `#!/bin/bash + +# Log Analyzer Pro v2.0 +# Advanced log analysis and reporting tool + +LOG_FILE="\${1:-/var/log/syslog}" +OUTPUT_DIR="\${2:-./reports}" +DATE_RANGE="\${3:-7}" + +mkdir -p "$OUTPUT_DIR" + +echo "🔍 Analyzing logs: $LOG_FILE" +echo "📊 Generating report for last $DATE_RANGE days" + +# Generate timestamp for report +REPORT_TIME=$(date +"%Y%m%d_%H%M%S") +REPORT_FILE="$OUTPUT_DIR/log_analysis_$REPORT_TIME.txt" + +echo "=== LOG ANALYSIS REPORT ===" > "$REPORT_FILE" +echo "Generated: $(date)" >> "$REPORT_FILE" +echo "Log file: $LOG_FILE" >> "$REPORT_FILE" +echo "=========================" >> "$REPORT_FILE" + +# Error analysis +echo "🚨 Error Analysis:" >> "$REPORT_FILE" +grep -i "error\\|fail\\|critical" "$LOG_FILE" | tail -20 >> "$REPORT_FILE" + +# Authentication attempts +echo "🔐 Authentication Events:" >> "$REPORT_FILE" +grep -i "auth\\|login\\|sudo" "$LOG_FILE" | tail -15 >> "$REPORT_FILE" + +# Network connections +echo "🌐 Network Activity:" >> "$REPORT_FILE" +grep -i "connection\\|network\\|ssh" "$LOG_FILE" | tail -10 >> "$REPORT_FILE" + +# Generate summary +TOTAL_LINES=$(wc -l < "$LOG_FILE") +ERROR_COUNT=$(grep -c -i "error" "$LOG_FILE") +WARNING_COUNT=$(grep -c -i "warning" "$LOG_FILE") + +echo "📈 Summary Statistics:" >> "$REPORT_FILE" +echo "Total log entries: $TOTAL_LINES" >> "$REPORT_FILE" +echo "Errors found: $ERROR_COUNT" >> "$REPORT_FILE" +echo "Warnings found: $WARNING_COUNT" >> "$REPORT_FILE" + +echo "✅ Analysis complete! Report saved: $REPORT_FILE"`, + compatible_os: ['linux', 'macos'], + categories: ['monitoring', 'security'], + tags: ['bash', 'logs', 'analysis', 'security'], + author_id: users[2].id, + author_name: users[2].display_name, + view_count: 123, + download_count: 45, + rating: 4.6, + rating_count: 7, + is_approved: true, + is_public: true, + version: '2.0.0', + }, + ]; + + return { users, scripts }; +}; + +async function setupDatabase() { + let connection; + + try { + console.log('🔌 Connecting to MariaDB server...'); + // First connect without specifying a database + const { database, ...dbConfigWithoutDb } = dbConfig; + connection = await mysql.createConnection(dbConfigWithoutDb); + + console.log('✅ Connected to MariaDB server successfully!'); + + // Create database if it doesn't exist + console.log('🗄️ Creating scriptshare database...'); + await connection.execute('CREATE DATABASE IF NOT EXISTS scriptshare'); + await connection.execute('USE scriptshare'); + console.log('✅ Database scriptshare is ready!'); + + // Create tables one by one + console.log('📊 Creating database tables...'); + for (let i = 0; i < createTableQueries.length; i++) { + const query = createTableQueries[i]; + const tableName = query.match(/CREATE TABLE IF NOT EXISTS (\w+)/)[1]; + console.log(` Creating table: ${tableName}`); + await connection.execute(query); + } + console.log('✅ All tables created successfully!'); + + // Generate and insert demo data + console.log('📝 Generating demo data...'); + const { users, scripts } = generateDemoData(); + + // Insert users + console.log('👥 Inserting demo users...'); + for (const user of users) { + await connection.execute( + 'INSERT IGNORE INTO users (id, email, username, display_name, avatar_url, bio, is_admin, is_moderator) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [ + user.id, + user.email, + user.username, + user.display_name, + user.avatar_url || null, + user.bio || null, + user.is_admin, + user.is_moderator + ] + ); + } + + // Insert scripts + console.log('📜 Inserting demo scripts...'); + for (const script of scripts) { + await connection.execute( + 'INSERT IGNORE INTO scripts (id, name, description, content, compatible_os, categories, tags, git_repository_url, author_id, author_name, view_count, download_count, rating, rating_count, is_approved, is_public, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + script.id, + script.name, + script.description, + script.content, + JSON.stringify(script.compatible_os), + JSON.stringify(script.categories), + JSON.stringify(script.tags || []), + script.git_repository_url || null, + script.author_id, + script.author_name, + script.view_count, + script.download_count, + script.rating, + script.rating_count, + script.is_approved, + script.is_public, + script.version + ] + ); + + // Insert script version + await connection.execute( + 'INSERT IGNORE INTO script_versions (id, script_id, version, content, changelog, created_by) VALUES (?, ?, ?, ?, ?, ?)', + [nanoid(), script.id, script.version, script.content, 'Initial version', script.author_id] + ); + } + + // Insert some demo ratings + console.log('⭐ Inserting demo ratings...'); + const ratings = [ + { script_id: scripts[0].id, user_id: users[0].id, rating: 5 }, + { script_id: scripts[0].id, user_id: users[2].id, rating: 4 }, + { script_id: scripts[1].id, user_id: users[0].id, rating: 5 }, + { script_id: scripts[1].id, user_id: users[1].id, rating: 4 }, + { script_id: scripts[2].id, user_id: users[2].id, rating: 4 }, + ]; + + for (const rating of ratings) { + await connection.execute( + 'INSERT IGNORE INTO ratings (id, script_id, user_id, rating) VALUES (?, ?, ?, ?)', + [nanoid(), rating.script_id, rating.user_id, rating.rating] + ); + } + + console.log('🎉 Database setup completed successfully!'); + console.log('📊 Demo data inserted:'); + console.log(` - ${users.length} users`); + console.log(` - ${scripts.length} scripts`); + console.log(` - ${ratings.length} ratings`); + + } catch (error) { + console.error('❌ Database setup failed:', error); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +// Run the setup +setupDatabase(); diff --git a/src/lib/api/analytics.ts b/src/lib/api/analytics.ts index 8fa4208..7f9ac43 100644 --- a/src/lib/api/analytics.ts +++ b/src/lib/api/analytics.ts @@ -1,30 +1,274 @@ -export interface TrackEventData { - scriptId: string; - eventType: string; - userId?: string; - userAgent?: string; - ipAddress?: string; - referrer?: string; -} -export interface AnalyticsFilters { - scriptId?: string; - eventType?: string; - startDate?: Date; - endDate?: Date; - userId?: string; -} -export async function trackEvent(data: TrackEventData) { - return { success: true }; -} -export async function getAnalyticsEvents(filters?: AnalyticsFilters) { - return []; -} -export async function getScriptAnalytics(scriptId: string, days?: number) { - return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 }; -} -export async function getPlatformAnalytics(days?: number) { - return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 }; -} -export async function getUserAnalytics(userId: string, days?: number) { - return { userScripts: [], recentActivity: [], periodDays: days || 30 }; -} \ No newline at end of file +import { db } from '@/lib/db'; +import { scriptAnalytics, scripts } from '@/lib/db/schema'; +import { eq, and, gte, lte, desc, count, sql } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface TrackEventData { + scriptId: string; + eventType: 'view' | 'download' | 'share'; + userId?: string; + userAgent?: string; + ipAddress?: string; + referrer?: string; +} + +export interface AnalyticsFilters { + scriptId?: string; + eventType?: string; + startDate?: Date; + endDate?: Date; + userId?: string; +} + +// Track an analytics event +export async function trackEvent(data: TrackEventData) { + try { + await db.insert(scriptAnalytics).values({ + id: generateId(), + scriptId: data.scriptId, + eventType: data.eventType, + userId: data.userId, + userAgent: data.userAgent, + ipAddress: data.ipAddress, + referrer: data.referrer, + createdAt: new Date(), + }); + + // Update script counters based on event type + if (data.eventType === 'view') { + await db + .update(scripts) + .set({ + viewCount: sql`${scripts.viewCount} + 1`, + }) + .where(eq(scripts.id, data.scriptId)); + } else if (data.eventType === 'download') { + await db + .update(scripts) + .set({ + downloadCount: sql`${scripts.downloadCount} + 1`, + }) + .where(eq(scripts.id, data.scriptId)); + } + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to track event: ${error}`, 500); + } +} + +// Get analytics events with filters +export async function getAnalyticsEvents(filters: AnalyticsFilters = {}) { + try { + let query = db.select().from(scriptAnalytics); + let conditions: any[] = []; + + if (filters.scriptId) { + conditions.push(eq(scriptAnalytics.scriptId, filters.scriptId)); + } + + if (filters.eventType) { + conditions.push(eq(scriptAnalytics.eventType, filters.eventType)); + } + + if (filters.userId) { + conditions.push(eq(scriptAnalytics.userId, filters.userId)); + } + + if (filters.startDate) { + conditions.push(gte(scriptAnalytics.createdAt, filters.startDate)); + } + + if (filters.endDate) { + conditions.push(lte(scriptAnalytics.createdAt, filters.endDate)); + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any; + } + + const events = await query.orderBy(desc(scriptAnalytics.createdAt)); + return events; + } catch (error) { + throw new ApiError(`Failed to get analytics events: ${error}`, 500); + } +} + +// Get analytics summary for a script +export async function getScriptAnalytics(scriptId: string, days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Get event counts by type + const eventCounts = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.eventType); + + // Get daily activity + const dailyActivity = await db + .select({ + date: sql`DATE(${scriptAnalytics.createdAt})`, + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(sql`DATE(${scriptAnalytics.createdAt})`, scriptAnalytics.eventType); + + // Get referrer statistics + const referrers = await db + .select({ + referrer: scriptAnalytics.referrer, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where( + and( + eq(scriptAnalytics.scriptId, scriptId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.referrer) + .orderBy(desc(count(scriptAnalytics.id))) + .limit(10); + + return { + eventCounts, + dailyActivity, + referrers, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get script analytics: ${error}`, 500); + } +} + +// Get platform-wide analytics (admin only) +export async function getPlatformAnalytics(days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // Total scripts and activity + const [totals] = await db + .select({ + totalScripts: count(scripts.id), + approvedScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`, + pendingScripts: sql`SUM(CASE WHEN ${scripts.isApproved} = 0 THEN 1 ELSE 0 END)`, + }) + .from(scripts); + + // Activity by event type + const activityByType = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .where(gte(scriptAnalytics.createdAt, startDate)) + .groupBy(scriptAnalytics.eventType); + + // Most popular scripts + const popularScripts = await db + .select({ + scriptId: scriptAnalytics.scriptId, + scriptName: scripts.name, + views: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) + .where( + and( + eq(scriptAnalytics.eventType, 'view'), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.scriptId, scripts.name) + .orderBy(desc(count(scriptAnalytics.id))) + .limit(10); + + // Daily activity trends + const dailyTrends = await db + .select({ + date: sql`DATE(${scriptAnalytics.createdAt})`, + views: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`, + downloads: sql`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'download' THEN 1 ELSE 0 END)`, + }) + .from(scriptAnalytics) + .where(gte(scriptAnalytics.createdAt, startDate)) + .groupBy(sql`DATE(${scriptAnalytics.createdAt})`) + .orderBy(sql`DATE(${scriptAnalytics.createdAt})`); + + return { + totals, + activityByType, + popularScripts, + dailyTrends, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get platform analytics: ${error}`, 500); + } +} + +// Get user analytics +export async function getUserAnalytics(userId: string, days: number = 30) { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // User's scripts performance + const userScriptsAnalytics = await db + .select({ + scriptId: scripts.id, + scriptName: scripts.name, + views: scripts.viewCount, + downloads: scripts.downloadCount, + rating: scripts.rating, + ratingCount: scripts.ratingCount, + }) + .from(scripts) + .where(eq(scripts.authorId, userId)) + .orderBy(desc(scripts.viewCount)); + + // Recent activity on user's scripts + const recentActivity = await db + .select({ + eventType: scriptAnalytics.eventType, + count: count(scriptAnalytics.id), + }) + .from(scriptAnalytics) + .innerJoin(scripts, eq(scriptAnalytics.scriptId, scripts.id)) + .where( + and( + eq(scripts.authorId, userId), + gte(scriptAnalytics.createdAt, startDate) + ) + ) + .groupBy(scriptAnalytics.eventType); + + return { + userScripts: userScriptsAnalytics, + recentActivity, + periodDays: days, + }; + } catch (error) { + throw new ApiError(`Failed to get user analytics: ${error}`, 500); + } +} diff --git a/src/lib/api/auth.ts b/src/lib/api/auth.ts index 7b08ce0..b4a69cc 100644 --- a/src/lib/api/auth.ts +++ b/src/lib/api/auth.ts @@ -1,26 +1,217 @@ -export interface LoginCredentials { - email: string; - password: string; -} -export interface RegisterData { - email: string; - username: string; - displayName: string; - password: string; -} -export interface AuthToken { - token: string; - user: any; -} -export async function login(credentials: LoginCredentials): Promise { - return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; -} -export async function register(data: RegisterData): Promise { - return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } }; -} -export async function refreshToken(token: string): Promise { - return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } }; -} -export async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise { - return true; -} \ No newline at end of file +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { getUserByEmail, getUserByUsername, createUser } from './users'; +import { ApiError } from './index'; + +export interface LoginCredentials { + email: string; + password: string; +} + +export interface RegisterData { + email: string; + username: string; + displayName: string; + password: string; +} + +export interface AuthToken { + token: string; + user: { + id: string; + email: string; + username: string; + displayName: string; + isAdmin: boolean; + isModerator: boolean; + }; +} + +const JWT_SECRET = process.env.JWT_SECRET || 'default-secret-key'; +const SALT_ROUNDS = 12; + +// Hash password +export async function hashPassword(password: string): Promise { + try { + return await bcrypt.hash(password, SALT_ROUNDS); + } catch (error) { + throw new ApiError('Failed to hash password', 500); + } +} + +// Verify password +export async function verifyPassword(password: string, hashedPassword: string): Promise { + try { + return await bcrypt.compare(password, hashedPassword); + } catch (error) { + throw new ApiError('Failed to verify password', 500); + } +} + +// Generate JWT token +export function generateToken(user: any): string { + const payload = { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin, + isModerator: user.isModerator, + }; + + return jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' }); +} + +// Verify JWT token +export function verifyToken(token: string): any { + try { + return jwt.verify(token, JWT_SECRET); + } catch (error) { + throw new ApiError('Invalid or expired token', 401); + } +} + +// Login user +export async function login(credentials: LoginCredentials): Promise { + try { + const user = await getUserByEmail(credentials.email); + if (!user) { + throw new ApiError('Invalid email or password', 401); + } + + // Note: In a real implementation, you would verify the password against a hash + // For this demo, we'll assume password verification passes + // const isValidPassword = await verifyPassword(credentials.password, user.passwordHash); + // if (!isValidPassword) { + // throw new ApiError('Invalid email or password', 401); + // } + + const token = generateToken(user); + + return { + token, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Login failed', 500); + } +} + +// Register user +export async function register(data: RegisterData): Promise { + try { + // Check if email already exists + const existingEmail = await getUserByEmail(data.email); + if (existingEmail) { + throw new ApiError('Email already registered', 400); + } + + // Check if username already exists + const existingUsername = await getUserByUsername(data.username); + if (existingUsername) { + throw new ApiError('Username already taken', 400); + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(data.email)) { + throw new ApiError('Invalid email format', 400); + } + + // Validate username format + const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; + if (!usernameRegex.test(data.username)) { + throw new ApiError('Username must be 3-20 characters and contain only letters, numbers, and underscores', 400); + } + + // Validate password strength + if (data.password.length < 6) { + throw new ApiError('Password must be at least 6 characters long', 400); + } + + // Hash password and create user + // const passwordHash = await hashPassword(data.password); + + const user = await createUser({ + email: data.email, + username: data.username, + displayName: data.displayName, + // passwordHash, // In a real implementation + }); + + const token = generateToken(user); + + return { + token, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Registration failed', 500); + } +} + +// Refresh token +export async function refreshToken(token: string): Promise { + try { + const decoded = verifyToken(token); + const user = await getUserByEmail(decoded.email); + + if (!user) { + throw new ApiError('User not found', 404); + } + + const newToken = generateToken(user); + + return { + token: newToken, + user: { + id: user.id, + email: user.email, + username: user.username, + displayName: user.displayName, + isAdmin: user.isAdmin || false, + isModerator: user.isModerator || false, + }, + }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Token refresh failed', 500); + } +} + +// Change password +export async function changePassword(_userId: string, _currentPassword: string, newPassword: string): Promise { + try { + // In a real implementation, you would: + // 1. Get user by ID + // 2. Verify current password + // 3. Hash new password + // 4. Update user record + + if (newPassword.length < 6) { + throw new ApiError('New password must be at least 6 characters long', 400); + } + + // Placeholder for password change logic + return true; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError('Password change failed', 500); + } +} diff --git a/src/lib/api/collections.ts b/src/lib/api/collections.ts index 178586c..ac40467 100644 --- a/src/lib/api/collections.ts +++ b/src/lib/api/collections.ts @@ -1,38 +1,274 @@ -export interface CreateCollectionData { - name: string; - description?: string; - authorId: string; - isPublic?: boolean; -} -export interface UpdateCollectionData { - name?: string; - description?: string; - isPublic?: boolean; -} -export async function createCollection(data: CreateCollectionData) { - return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() }; -} -export async function getCollectionById(id: string) { - return null; -} -export async function getUserCollections(userId: string) { - return []; -} -export async function getPublicCollections(limit?: number, offset?: number) { - return []; -} -export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { - return { id, ...data, updatedAt: new Date() }; -} -export async function deleteCollection(id: string, userId: string) { - return { success: true }; -} -export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { - return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() }; -} -export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { - return { success: true }; -} -export async function isScriptInCollection(collectionId: string, scriptId: string) { - return false; -} \ No newline at end of file +import { db } from '@/lib/db'; +import { scriptCollections, collectionScripts } from '@/lib/db/schema'; +import { eq, and, desc } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateCollectionData { + name: string; + description?: string; + authorId: string; + isPublic?: boolean; +} + +export interface UpdateCollectionData { + name?: string; + description?: string; + isPublic?: boolean; +} + +// Create a new collection +export async function createCollection(data: CreateCollectionData) { + try { + const collectionId = generateId(); + const now = new Date(); + + await db.insert(scriptCollections).values({ + id: collectionId, + name: data.name, + description: data.description, + authorId: data.authorId, + isPublic: data.isPublic ?? true, + createdAt: now, + updatedAt: now, + }); + + const collection = { + id: collectionId, + name: data.name, + description: data.description, + authorId: data.authorId, + isPublic: data.isPublic ?? true, + createdAt: now, + updatedAt: now, + }; + + return collection; + } catch (error) { + throw new ApiError(`Failed to create collection: ${error}`, 500); + } +} + +// Get collection by ID +export async function getCollectionById(id: string) { + try { + const collection = await db.query.scriptCollections.findFirst({ + where: eq(scriptCollections.id, id), + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + scripts: { + with: { + script: { + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + }, + }, + }, + orderBy: desc(collectionScripts.addedAt), + }, + }, + }); + + if (!collection) { + throw new ApiError('Collection not found', 404); + } + + return collection; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get collection: ${error}`, 500); + } +} + +// Get collections by user +export async function getUserCollections(userId: string) { + try { + const collections = await db.query.scriptCollections.findMany({ + where: eq(scriptCollections.authorId, userId), + with: { + scripts: { + with: { + script: true, + }, + }, + }, + orderBy: desc(scriptCollections.createdAt), + }); + + return collections; + } catch (error) { + throw new ApiError(`Failed to get user collections: ${error}`, 500); + } +} + +// Get public collections +export async function getPublicCollections(limit: number = 20, offset: number = 0) { + try { + const collections = await db.query.scriptCollections.findMany({ + where: eq(scriptCollections.isPublic, true), + with: { + author: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + scripts: { + with: { + script: true, + }, + limit: 5, // Preview of scripts in collection + }, + }, + orderBy: desc(scriptCollections.createdAt), + limit, + offset, + }); + + return collections; + } catch (error) { + throw new ApiError(`Failed to get public collections: ${error}`, 500); + } +} + +// Update collection +export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(id); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to update this collection', 403); + } + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(scriptCollections) + .set(updateData) + .where(eq(scriptCollections.id, id)); + + const updatedCollection = { ...collection, ...updateData }; + + return updatedCollection; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to update collection: ${error}`, 500); + } +} + +// Delete collection +export async function deleteCollection(id: string, userId: string) { + try { + const collection = await getCollectionById(id); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to delete this collection', 403); + } + + // Delete all scripts in collection first + await db.delete(collectionScripts).where(eq(collectionScripts.collectionId, id)); + + // Delete the collection + await db.delete(scriptCollections).where(eq(scriptCollections.id, id)); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to delete collection: ${error}`, 500); + } +} + +// Add script to collection +export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(collectionId); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to modify this collection', 403); + } + + // Check if script is already in collection + const existing = await db.query.collectionScripts.findFirst({ + where: and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ), + }); + + if (existing) { + throw new ApiError('Script is already in this collection', 400); + } + + const collectionScriptData = { + id: generateId(), + collectionId, + scriptId, + addedAt: new Date(), + }; + + await db.insert(collectionScripts).values(collectionScriptData); + + return collectionScriptData; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to add script to collection: ${error}`, 500); + } +} + +// Remove script from collection +export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) { + try { + // Check if user owns the collection + const collection = await getCollectionById(collectionId); + if (collection.authorId !== userId) { + throw new ApiError('Unauthorized to modify this collection', 403); + } + + await db + .delete(collectionScripts) + .where( + and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ) + ); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to remove script from collection: ${error}`, 500); + } +} + +// Check if script is in collection +export async function isScriptInCollection(collectionId: string, scriptId: string) { + try { + const collectionScript = await db.query.collectionScripts.findFirst({ + where: and( + eq(collectionScripts.collectionId, collectionId), + eq(collectionScripts.scriptId, scriptId) + ), + }); + + return !!collectionScript; + } catch (error) { + throw new ApiError(`Failed to check if script is in collection: ${error}`, 500); + } +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 0fc0b63..1ff4501 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,14 +1,20 @@ -import { nanoid } from "nanoid"; -export const generateId = () => nanoid(); -export class ApiError extends Error { - constructor(message: string, public status: number = 500) { - super(message); - this.name = "ApiError"; - } -} -export * from "./scripts"; -export * from "./users"; -export * from "./ratings"; -export * from "./analytics"; -export * from "./collections"; -export * from "./auth"; \ No newline at end of file +import { nanoid } from 'nanoid'; + +// Generate unique IDs +export const generateId = () => nanoid(); + +// Error handling +export class ApiError extends Error { + constructor(message: string, public status: number = 500) { + super(message); + this.name = 'ApiError'; + } +} + +// Export all service modules +export * from './scripts'; +export * from './users'; +export * from './ratings'; +export * from './analytics'; +export * from './collections'; +export * from './auth'; diff --git a/src/lib/api/mock.ts b/src/lib/api/mock.ts new file mode 100644 index 0000000..4c725bf --- /dev/null +++ b/src/lib/api/mock.ts @@ -0,0 +1,45 @@ +// Mock API implementations for demo purposes +// In a real app, these would be actual database operations + +import { generateId } from './index'; + +// For demo purposes, we'll use these mock functions instead of real database calls +// This avoids the MySQL-specific .returning() issues and provides working functionality + +export const mockApiResponses = { + createScript: (data: any) => ({ + id: generateId(), + ...data, + isApproved: false, + isPublic: true, + viewCount: 0, + downloadCount: 0, + rating: 0, + ratingCount: 0, + createdAt: new Date(), + updatedAt: new Date(), + }), + + createUser: (data: any) => ({ + id: generateId(), + ...data, + isAdmin: false, + isModerator: false, + createdAt: new Date(), + updatedAt: new Date(), + }), + + createRating: (data: any) => ({ + id: generateId(), + ...data, + createdAt: new Date(), + updatedAt: new Date(), + }), + + createCollection: (data: any) => ({ + id: generateId(), + ...data, + createdAt: new Date(), + updatedAt: new Date(), + }), +}; diff --git a/src/lib/api/ratings.ts b/src/lib/api/ratings.ts index 41e71f3..a9721f2 100644 --- a/src/lib/api/ratings.ts +++ b/src/lib/api/ratings.ts @@ -1,20 +1,191 @@ -export interface CreateRatingData { - scriptId: string; - userId: string; - rating: number; -} -export async function rateScript(data: CreateRatingData) { - return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() }; -} -export async function getUserRating(scriptId: string, userId: string) { - return null; -} -export async function getScriptRatings(scriptId: string) { - return []; -} -export async function getScriptRatingStats(scriptId: string) { - return { averageRating: 0, totalRatings: 0, distribution: [] }; -} -export async function deleteRating(scriptId: string, userId: string) { - return { success: true }; -} \ No newline at end of file +import { db } from '@/lib/db'; +import { ratings, scripts } from '@/lib/db/schema'; +import { eq, and, avg, count } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateRatingData { + scriptId: string; + userId: string; + rating: number; // 1-5 stars +} + +// Create or update a rating +export async function rateScript(data: CreateRatingData) { + try { + if (data.rating < 1 || data.rating > 5) { + throw new ApiError('Rating must be between 1 and 5', 400); + } + + // Check if user already rated this script + const existingRating = await db.query.ratings.findFirst({ + where: and( + eq(ratings.scriptId, data.scriptId), + eq(ratings.userId, data.userId) + ), + }); + + let ratingRecord; + if (existingRating) { + // Update existing rating + await db + .update(ratings) + .set({ + rating: data.rating, + updatedAt: new Date(), + }) + .where(eq(ratings.id, existingRating.id)); + + ratingRecord = { + ...existingRating, + rating: data.rating, + updatedAt: new Date(), + }; + } else { + // Create new rating + ratingRecord = { + id: generateId(), + scriptId: data.scriptId, + userId: data.userId, + rating: data.rating, + createdAt: new Date(), + updatedAt: new Date(), + }; + + await db.insert(ratings).values(ratingRecord); + } + + // Update script's average rating and count + await updateScriptRating(data.scriptId); + + return ratingRecord; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to rate script: ${error}`, 500); + } +} + +// Get user's rating for a script +export async function getUserRating(scriptId: string, userId: string) { + try { + const userRating = await db.query.ratings.findFirst({ + where: and( + eq(ratings.scriptId, scriptId), + eq(ratings.userId, userId) + ), + }); + + return userRating; + } catch (error) { + throw new ApiError(`Failed to get user rating: ${error}`, 500); + } +} + +// Get all ratings for a script +export async function getScriptRatings(scriptId: string) { + try { + const scriptRatings = await db.query.ratings.findMany({ + where: eq(ratings.scriptId, scriptId), + with: { + user: { + columns: { + id: true, + username: true, + displayName: true, + avatarUrl: true, + }, + }, + }, + }); + + return scriptRatings; + } catch (error) { + throw new ApiError(`Failed to get script ratings: ${error}`, 500); + } +} + +// Update script's average rating and count +async function updateScriptRating(scriptId: string) { + try { + const [stats] = await db + .select({ + avgRating: avg(ratings.rating), + ratingCount: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)); + + const avgRating = stats.avgRating ? Math.round(Number(stats.avgRating) * 10) / 10 : 0; + const ratingCount = stats.ratingCount || 0; + + await db + .update(scripts) + .set({ + rating: avgRating, + ratingCount: ratingCount, + }) + .where(eq(scripts.id, scriptId)); + + return { avgRating, ratingCount }; + } catch (error) { + throw new ApiError(`Failed to update script rating: ${error}`, 500); + } +} + +// Delete a rating +export async function deleteRating(scriptId: string, userId: string) { + try { + await db + .delete(ratings) + .where( + and( + eq(ratings.scriptId, scriptId), + eq(ratings.userId, userId) + ) + ); + + // Update script's average rating and count + await updateScriptRating(scriptId); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to delete rating: ${error}`, 500); + } +} + +// Get rating statistics for a script +export async function getScriptRatingStats(scriptId: string) { + try { + const stats = await db + .select({ + rating: ratings.rating, + count: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)) + .groupBy(ratings.rating); + + const distribution = [1, 2, 3, 4, 5].map(star => { + const found = stats.find(s => s.rating === star); + return { + stars: star, + count: found ? found.count : 0, + }; + }); + + const [totals] = await db + .select({ + avgRating: avg(ratings.rating), + totalRatings: count(ratings.id), + }) + .from(ratings) + .where(eq(ratings.scriptId, scriptId)); + + return { + averageRating: totals.avgRating ? Math.round(Number(totals.avgRating) * 10) / 10 : 0, + totalRatings: totals.totalRatings || 0, + distribution, + }; + } catch (error) { + throw new ApiError(`Failed to get rating stats: ${error}`, 500); + } +} diff --git a/src/lib/api/scripts.ts b/src/lib/api/scripts.ts index 5eaf920..f4c3ded 100644 --- a/src/lib/api/scripts.ts +++ b/src/lib/api/scripts.ts @@ -1,51 +1,367 @@ -export interface ScriptFilters { - search?: string; - categories?: string[]; - compatibleOs?: string[]; - sortBy?: string; - limit?: number; - isApproved?: boolean; -} -export interface UpdateScriptData { - name?: string; - description?: string; - content?: string; -} -export interface CreateScriptData { - name: string; - description: string; - content: string; - categories: string[]; - compatibleOs: string[]; - tags?: string[]; -} -export async function getScripts(filters?: ScriptFilters) { - return { scripts: [], total: 0 }; -} -export async function getScriptById(id: string) { - return null; -} -export async function getPopularScripts() { - return []; -} -export async function getRecentScripts() { - return []; -} -export async function createScript(data: CreateScriptData, userId: string) { - return { id: "mock-script-id", ...data, authorId: userId }; -} -export async function updateScript(id: string, data: UpdateScriptData, userId: string) { - return { id, ...data }; -} -export async function deleteScript(id: string, userId: string) { - return { success: true }; -} -export async function moderateScript(id: string, isApproved: boolean, moderatorId: string) { - return { id, isApproved }; -} -export async function incrementViewCount(id: string) { - return { success: true }; -} -export async function incrementDownloadCount(id: string) { - return { success: true }; -} \ No newline at end of file +import { db } from '@/lib/db'; +import { scripts, scriptVersions, ratings } from '@/lib/db/schema'; +import { eq, desc, asc, and, or, like, count, sql } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateScriptData { + name: string; + description: string; + content: string; + compatibleOs: string[]; + categories: string[]; + tags?: string[]; + gitRepositoryUrl?: string; + authorId: string; + authorName: string; + version?: string; +} + +export interface UpdateScriptData { + name?: string; + description?: string; + content?: string; + compatibleOs?: string[]; + categories?: string[]; + tags?: string[]; + gitRepositoryUrl?: string; + version?: string; +} + +export interface ScriptFilters { + categories?: string[]; + compatibleOs?: string[]; + search?: string; + authorId?: string; + isApproved?: boolean; + sortBy?: 'newest' | 'oldest' | 'popular' | 'rating'; + limit?: number; + offset?: number; +} + +// Create a new script +export async function createScript(data: CreateScriptData) { + try { + const scriptId = generateId(); + const now = new Date(); + + await db.insert(scripts).values({ + id: scriptId, + name: data.name, + description: data.description, + content: data.content, + compatibleOs: data.compatibleOs, + categories: data.categories, + tags: data.tags || [], + gitRepositoryUrl: data.gitRepositoryUrl, + authorId: data.authorId, + authorName: data.authorName, + version: data.version || '1.0.0', + isApproved: false, + isPublic: true, + viewCount: 0, + downloadCount: 0, + rating: 0, + ratingCount: 0, + createdAt: now, + updatedAt: now, + }); + + const script = { + id: scriptId, + name: data.name, + description: data.description, + content: data.content, + compatibleOs: data.compatibleOs, + categories: data.categories, + tags: data.tags || [], + gitRepositoryUrl: data.gitRepositoryUrl, + authorId: data.authorId, + authorName: data.authorName, + version: data.version || '1.0.0', + isApproved: false, + isPublic: true, + viewCount: 0, + downloadCount: 0, + rating: 0, + ratingCount: 0, + createdAt: now, + updatedAt: now, + }; + + // Create initial version + await db.insert(scriptVersions).values({ + id: generateId(), + scriptId: scriptId, + version: data.version || '1.0.0', + content: data.content, + changelog: 'Initial version', + createdAt: now, + createdBy: data.authorId, + }); + + return script; + } catch (error) { + throw new ApiError(`Failed to create script: ${error}`, 500); + } +} + +// Get script by ID +export async function getScriptById(id: string) { + try { + const script = await db.query.scripts.findFirst({ + where: eq(scripts.id, id), + with: { + author: true, + versions: { + orderBy: desc(scriptVersions.createdAt), + }, + ratings: true, + }, + }); + + if (!script) { + throw new ApiError('Script not found', 404); + } + + return script; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get script: ${error}`, 500); + } +} + +// Get scripts with filters +export async function getScripts(filters: ScriptFilters = {}) { + try { + const { + categories, + compatibleOs, + search, + authorId, + isApproved = true, + sortBy = 'newest', + limit = 20, + offset = 0, + } = filters; + + let query = db.select().from(scripts); + let conditions: any[] = []; + + // Apply filters + if (isApproved !== undefined) { + conditions.push(eq(scripts.isApproved, isApproved)); + } + + if (authorId) { + conditions.push(eq(scripts.authorId, authorId)); + } + + if (search) { + conditions.push( + or( + like(scripts.name, `%${search}%`), + like(scripts.description, `%${search}%`) + ) + ); + } + + if (categories && categories.length > 0) { + conditions.push( + sql`JSON_OVERLAPS(${scripts.categories}, ${JSON.stringify(categories)})` + ); + } + + if (compatibleOs && compatibleOs.length > 0) { + conditions.push( + sql`JSON_OVERLAPS(${scripts.compatibleOs}, ${JSON.stringify(compatibleOs)})` + ); + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any; + } + + // Apply sorting + switch (sortBy) { + case 'newest': + query = query.orderBy(desc(scripts.createdAt)) as any; + break; + case 'oldest': + query = query.orderBy(asc(scripts.createdAt)) as any; + break; + case 'popular': + query = query.orderBy(desc(scripts.viewCount)) as any; + break; + case 'rating': + query = query.orderBy(desc(scripts.rating)) as any; + break; + } + + // Apply pagination + query = query.limit(limit).offset(offset) as any; + + const results = await query; + + // Get total count for pagination + const [{ total }] = await db + .select({ total: count() }) + .from(scripts) + .where(conditions.length > 0 ? and(...conditions) : undefined); + + return { + scripts: results, + total, + hasMore: offset + limit < total, + }; + } catch (error) { + throw new ApiError(`Failed to get scripts: ${error}`, 500); + } +} + +// Update script +export async function updateScript(id: string, data: UpdateScriptData, userId: string) { + try { + // Check if user owns the script or is admin + const script = await getScriptById(id); + if (script.authorId !== userId) { + throw new ApiError('Unauthorized to update this script', 403); + } + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(scripts) + .set(updateData) + .where(eq(scripts.id, id)); + + const updatedScript = { ...script, ...updateData }; + + // If content changed, create new version + if (data.content && data.version) { + await db.insert(scriptVersions).values({ + id: generateId(), + scriptId: id, + version: data.version, + content: data.content, + changelog: 'Updated script content', + createdAt: new Date(), + createdBy: userId, + }); + } + + return updatedScript; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to update script: ${error}`, 500); + } +} + +// Delete script +export async function deleteScript(id: string, userId: string) { + try { + const script = await getScriptById(id); + if (script.authorId !== userId) { + throw new ApiError('Unauthorized to delete this script', 403); + } + + // Delete all related data + await db.delete(scriptVersions).where(eq(scriptVersions.scriptId, id)); + await db.delete(ratings).where(eq(ratings.scriptId, id)); + await db.delete(scripts).where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to delete script: ${error}`, 500); + } +} + +// Approve/reject script (admin only) +export async function moderateScript(id: string, isApproved: boolean, _moderatorId: string) { + try { + const script = await getScriptById(id); + if (!script) { + throw new ApiError('Script not found', 404); + } + + await db + .update(scripts) + .set({ + isApproved, + updatedAt: new Date(), + }) + .where(eq(scripts.id, id)); + + const moderatedScript = { ...script, isApproved, updatedAt: new Date() }; + return moderatedScript; + } catch (error) { + throw new ApiError(`Failed to moderate script: ${error}`, 500); + } +} + +// Increment view count +export async function incrementViewCount(id: string) { + try { + await db + .update(scripts) + .set({ + viewCount: sql`${scripts.viewCount} + 1`, + }) + .where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to increment view count: ${error}`, 500); + } +} + +// Increment download count +export async function incrementDownloadCount(id: string) { + try { + await db + .update(scripts) + .set({ + downloadCount: sql`${scripts.downloadCount} + 1`, + }) + .where(eq(scripts.id, id)); + + return { success: true }; + } catch (error) { + throw new ApiError(`Failed to increment download count: ${error}`, 500); + } +} + +// Get popular scripts +export async function getPopularScripts(limit: number = 10) { + try { + const popularScripts = await db + .select() + .from(scripts) + .where(eq(scripts.isApproved, true)) + .orderBy(desc(scripts.viewCount)) + .limit(limit); + + return popularScripts; + } catch (error) { + throw new ApiError(`Failed to get popular scripts: ${error}`, 500); + } +} + +// Get recent scripts +export async function getRecentScripts(limit: number = 10) { + try { + const recentScripts = await db + .select() + .from(scripts) + .where(eq(scripts.isApproved, true)) + .orderBy(desc(scripts.createdAt)) + .limit(limit); + + return recentScripts; + } catch (error) { + throw new ApiError(`Failed to get recent scripts: ${error}`, 500); + } +} diff --git a/src/lib/api/users.ts b/src/lib/api/users.ts index 49852a1..7486132 100644 --- a/src/lib/api/users.ts +++ b/src/lib/api/users.ts @@ -1,37 +1,174 @@ -export interface CreateUserData { - email: string; - username: string; - displayName: string; - avatarUrl?: string; - bio?: string; -} -export interface UpdateUserData { - username?: string; - displayName?: string; - avatarUrl?: string; - bio?: string; -} -export async function createUser(data: CreateUserData) { - return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() }; -} -export async function getUserById(id: string) { - return null; -} -export async function getUserByEmail(email: string) { - return null; -} -export async function getUserByUsername(username: string) { - return null; -} -export async function updateUser(id: string, data: UpdateUserData) { - return { id, ...data, updatedAt: new Date() }; -} -export async function updateUserPermissions(id: string, permissions: any) { - return { id, ...permissions, updatedAt: new Date() }; -} -export async function searchUsers(query: string, limit?: number) { - return []; -} -export async function getAllUsers(limit?: number, offset?: number) { - return []; -} \ No newline at end of file +import { db } from '@/lib/db'; +import { users } from '@/lib/db/schema'; +import { eq, like } from 'drizzle-orm'; +import { generateId, ApiError } from './index'; + +export interface CreateUserData { + email: string; + username: string; + displayName: string; + avatarUrl?: string; + bio?: string; +} + +export interface UpdateUserData { + username?: string; + displayName?: string; + avatarUrl?: string; + bio?: string; +} + +// Create a new user +export async function createUser(data: CreateUserData) { + try { + const userId = generateId(); + const now = new Date(); + + const userData = { + id: userId, + email: data.email, + username: data.username, + displayName: data.displayName, + avatarUrl: data.avatarUrl || null, + bio: data.bio || null, + isAdmin: false, + isModerator: false, + passwordHash: '', // This should be set by auth layer + createdAt: now, + updatedAt: now, + }; + + await db.insert(users).values(userData); + return userData; + } catch (error) { + throw new ApiError(`Failed to create user: ${error}`, 500); + } +} + +// Get user by ID +export async function getUserById(id: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.id, id), + with: { + scripts: { + where: eq(users.isAdmin, true) ? undefined : eq(users.id, id), // Only show own scripts unless admin + }, + }, + }); + + if (!user) { + throw new ApiError('User not found', 404); + } + + return user; + } catch (error) { + if (error instanceof ApiError) throw error; + throw new ApiError(`Failed to get user: ${error}`, 500); + } +} + +// Get user by email +export async function getUserByEmail(email: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + return user; + } catch (error) { + throw new ApiError(`Failed to get user by email: ${error}`, 500); + } +} + +// Get user by username +export async function getUserByUsername(username: string) { + try { + const user = await db.query.users.findFirst({ + where: eq(users.username, username), + }); + + return user; + } catch (error) { + throw new ApiError(`Failed to get user by username: ${error}`, 500); + } +} + +// Update user +export async function updateUser(id: string, data: UpdateUserData) { + try { + const user = await getUserById(id); + + const updateData = { + ...data, + updatedAt: new Date(), + }; + + await db + .update(users) + .set(updateData) + .where(eq(users.id, id)); + + const updatedUser = { ...user, ...updateData }; + return updatedUser; + } catch (error) { + throw new ApiError(`Failed to update user: ${error}`, 500); + } +} + +// Update user permissions (admin only) +export async function updateUserPermissions( + id: string, + permissions: { isAdmin?: boolean; isModerator?: boolean } +) { + try { + const user = await getUserById(id); + + const updateData = { + ...permissions, + updatedAt: new Date(), + }; + + await db + .update(users) + .set(updateData) + .where(eq(users.id, id)); + + const updatedUser = { ...user, ...updateData }; + return updatedUser; + } catch (error) { + throw new ApiError(`Failed to update user permissions: ${error}`, 500); + } +} + +// Search users +export async function searchUsers(query: string, limit: number = 20) { + try { + const searchResults = await db + .select() + .from(users) + .where( + like(users.username, `%${query}%`) + ) + .limit(limit); + + return searchResults; + } catch (error) { + throw new ApiError(`Failed to search users: ${error}`, 500); + } +} + +// Get all users (admin only) +export async function getAllUsers(limit: number = 50, offset: number = 0) { + try { + const allUsers = await db + .select() + .from(users) + .limit(limit) + .offset(offset); + + return allUsers; + } catch (error) { + throw new ApiError(`Failed to get all users: ${error}`, 500); + } +} diff --git a/src/lib/db/browser.ts b/src/lib/db/browser.ts new file mode 100644 index 0000000..0882a95 --- /dev/null +++ b/src/lib/db/browser.ts @@ -0,0 +1,33 @@ +// Browser-compatible database interface +// This provides mock implementations for browser builds + +export const db = { + query: { + users: { + findFirst: () => Promise.resolve(null), + findMany: () => Promise.resolve([]), + }, + scripts: { + findFirst: () => Promise.resolve(null), + findMany: () => Promise.resolve([]), + }, + }, + select: () => ({ from: () => ({ where: () => Promise.resolve([]) }) }), + insert: () => ({ values: () => Promise.resolve() }), + update: () => ({ set: () => ({ where: () => Promise.resolve() }) }), + delete: () => ({ where: () => Promise.resolve() }), +}; + +// Export schema as empty objects for browser compatibility +export const users = {}; +export const scripts = {}; +export const ratings = {}; +export const scriptVersions = {}; +export const scriptAnalytics = {}; +export const scriptCollections = {}; +export const collectionScripts = {}; + +// Export empty relations +export const usersRelations = {}; +export const scriptsRelations = {}; +export const ratingsRelations = {}; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 04cb237..5529de4 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1 +1,88 @@ -export const db = {}; \ No newline at end of file +import { drizzle } from 'drizzle-orm/mysql2'; +import mysql from 'mysql2/promise'; +import * as schema from './schema'; + +// Database configuration +const dbConfig = { + host: process.env.DB_HOST || '192.168.1.146', + port: parseInt(process.env.DB_PORT || '5444'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || 'j3bv5YmVN4CVwLmoMV6oVIMF62hhc8pBRaSWrIWvLIKIdZOAkNFbUa3ntKwCKABC', + database: process.env.DB_NAME || 'scriptshare', +}; + +// Connection pool +let connectionPool: mysql.Pool | null = null; +let dbInstance: any = null; + +// Initialize database connection +async function initializeDb() { + if (!connectionPool) { + try { + connectionPool = mysql.createPool({ + ...dbConfig, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + }); + + dbInstance = drizzle(connectionPool, { schema, mode: 'default' }); + console.log('✅ Database connection pool created'); + } catch (error) { + console.error('❌ Database connection failed:', error); + throw error; + } + } + return dbInstance; +} + +// Get database instance (lazy initialization) +async function getDbInstance() { + if (!dbInstance) { + await initializeDb(); + } + return dbInstance; +} + +// Export database instance with lazy loading +export const db = new Proxy({} as any, { + get(target, prop) { + return async (...args: any[]) => { + const dbConn = await getDbInstance(); + const result = dbConn[prop]; + if (typeof result === 'function') { + return result.apply(dbConn, args); + } + return result; + }; + } +}); + +// Export the schema for use in other parts of the app +export * from './schema'; + +// Test the connection +export const testConnection = async () => { + try { + const connection = await mysql.createConnection(dbConfig); + await connection.ping(); + await connection.end(); + console.log('✅ Database connection test successful'); + return true; + } catch (error) { + console.error('❌ Database connection test failed:', error); + return false; + } +}; + +// Initialize database tables +export const initializeTables = async () => { + try { + const dbConn = await getDbInstance(); + console.log('📊 Database tables initialized'); + return true; + } catch (error) { + console.error('❌ Failed to initialize tables:', error); + return false; + } +}; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index d5e0982..0740ea0 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -1 +1,186 @@ -export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {}; \ No newline at end of file +import { mysqlTable, varchar, text, timestamp, int, boolean, json, index } from 'drizzle-orm/mysql-core'; +import { relations } from 'drizzle-orm'; + +// Users table +export const users = mysqlTable('users', { + id: varchar('id', { length: 255 }).primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + username: varchar('username', { length: 100 }).notNull().unique(), + displayName: varchar('display_name', { length: 100 }).notNull(), + avatarUrl: varchar('avatar_url', { length: 500 }), + bio: text('bio'), + isAdmin: boolean('is_admin').default(false), + isModerator: boolean('is_moderator').default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + emailIdx: index('email_idx').on(table.email), + usernameIdx: index('username_idx').on(table.username), +})); + +// Scripts table +export const scripts = mysqlTable('scripts', { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 200 }).notNull(), + description: text('description').notNull(), + content: text('content').notNull(), + compatibleOs: json('compatible_os').$type().notNull(), + categories: json('categories').$type().notNull(), + tags: json('tags').$type(), + gitRepositoryUrl: varchar('git_repository_url', { length: 500 }), + authorId: varchar('author_id', { length: 255 }).notNull(), + authorName: varchar('author_name', { length: 100 }).notNull(), + viewCount: int('view_count').default(0).notNull(), + downloadCount: int('download_count').default(0).notNull(), + rating: int('rating').default(0).notNull(), + ratingCount: int('rating_count').default(0).notNull(), + isApproved: boolean('is_approved').default(false).notNull(), + isPublic: boolean('is_public').default(true).notNull(), + version: varchar('version', { length: 20 }).default('1.0.0').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + authorIdx: index('author_idx').on(table.authorId), + approvedIdx: index('approved_idx').on(table.isApproved), + publicIdx: index('public_idx').on(table.isPublic), + createdAtIdx: index('created_at_idx').on(table.createdAt), +})); + +// Script versions table +export const scriptVersions = mysqlTable('script_versions', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + version: varchar('version', { length: 20 }).notNull(), + content: text('content').notNull(), + changelog: text('changelog'), + createdAt: timestamp('created_at').defaultNow().notNull(), + createdBy: varchar('created_by', { length: 255 }).notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + versionIdx: index('version_idx').on(table.version), +})); + + + +// Ratings table +export const ratings = mysqlTable('ratings', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + userId: varchar('user_id', { length: 255 }).notNull(), + rating: int('rating').notNull(), // 1-5 stars + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + userIdx: index('user_idx').on(table.userId), + uniqueRating: index('unique_rating').on(table.scriptId, table.userId), +})); + +// Script collections table +export const scriptCollections = mysqlTable('script_collections', { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 200 }).notNull(), + description: text('description'), + authorId: varchar('author_id', { length: 255 }).notNull(), + isPublic: boolean('is_public').default(true).notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + authorIdx: index('author_idx').on(table.authorId), + publicIdx: index('public_idx').on(table.isPublic), +})); + +// Collection scripts junction table +export const collectionScripts = mysqlTable('collection_scripts', { + id: varchar('id', { length: 255 }).primaryKey(), + collectionId: varchar('collection_id', { length: 255 }).notNull(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + addedAt: timestamp('added_at').defaultNow().notNull(), +}, (table) => ({ + collectionIdx: index('collection_idx').on(table.collectionId), + scriptIdx: index('script_idx').on(table.scriptId), +})); + +// Script analytics table +export const scriptAnalytics = mysqlTable('script_analytics', { + id: varchar('id', { length: 255 }).primaryKey(), + scriptId: varchar('script_id', { length: 255 }).notNull(), + eventType: varchar('event_type', { length: 50 }).notNull(), // view, download, share + userId: varchar('user_id', { length: 255 }), + userAgent: text('user_agent'), + ipAddress: varchar('ip_address', { length: 45 }), + referrer: varchar('referrer', { length: 500 }), + createdAt: timestamp('created_at').defaultNow().notNull(), +}, (table) => ({ + scriptIdx: index('script_idx').on(table.scriptId), + eventIdx: index('event_idx').on(table.eventType), + userIdx: index('user_idx').on(table.userId), + createdAtIdx: index('created_at_idx').on(table.createdAt), +})); + +// Define relationships +export const usersRelations = relations(users, ({ many }) => ({ + scripts: many(scripts), + ratings: many(ratings), + collections: many(scriptCollections), +})); + +export const scriptsRelations = relations(scripts, ({ one, many }) => ({ + author: one(users, { + fields: [scripts.authorId], + references: [users.id], + }), + versions: many(scriptVersions), + ratings: many(ratings), + analytics: many(scriptAnalytics), +})); + +export const scriptVersionsRelations = relations(scriptVersions, ({ one }) => ({ + script: one(scripts, { + fields: [scriptVersions.scriptId], + references: [scripts.id], + }), +})); + + + +export const ratingsRelations = relations(ratings, ({ one }) => ({ + script: one(scripts, { + fields: [ratings.scriptId], + references: [scripts.id], + }), + user: one(users, { + fields: [ratings.userId], + references: [users.id], + }), +})); + +export const scriptCollectionsRelations = relations(scriptCollections, ({ one, many }) => ({ + author: one(users, { + fields: [scriptCollections.authorId], + references: [users.id], + }), + scripts: many(collectionScripts), +})); + +export const collectionScriptsRelations = relations(collectionScripts, ({ one }) => ({ + collection: one(scriptCollections, { + fields: [collectionScripts.collectionId], + references: [scriptCollections.id], + }), + script: one(scripts, { + fields: [collectionScripts.scriptId], + references: [scripts.id], + }), +})); + +export const scriptAnalyticsRelations = relations(scriptAnalytics, ({ one }) => ({ + script: one(scripts, { + fields: [scriptAnalytics.scriptId], + references: [scripts.id], + }), + user: one(users, { + fields: [scriptAnalytics.userId], + references: [users.id], + }), +}));