Update package dependencies and refactor API files to implement database interactions for analytics, authentication, collections, ratings, and scripts. Enhance user management and script handling with improved error handling and validation. Introduce database schema for structured data storage and retrieval.
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@ -49,7 +49,7 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"mysql2": "^3.12.0",
|
"mysql2": "^3.14.3",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"mysql2": "^3.12.0",
|
"mysql2": "^3.14.3",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
490
setup-database-v2.cjs
Normal file
490
setup-database-v2.cjs
Normal file
@ -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();
|
@ -1,30 +1,274 @@
|
|||||||
export interface TrackEventData {
|
import { db } from '@/lib/db';
|
||||||
scriptId: string;
|
import { scriptAnalytics, scripts } from '@/lib/db/schema';
|
||||||
eventType: string;
|
import { eq, and, gte, lte, desc, count, sql } from 'drizzle-orm';
|
||||||
userId?: string;
|
import { generateId, ApiError } from './index';
|
||||||
userAgent?: string;
|
|
||||||
ipAddress?: string;
|
export interface TrackEventData {
|
||||||
referrer?: string;
|
scriptId: string;
|
||||||
}
|
eventType: 'view' | 'download' | 'share';
|
||||||
export interface AnalyticsFilters {
|
userId?: string;
|
||||||
scriptId?: string;
|
userAgent?: string;
|
||||||
eventType?: string;
|
ipAddress?: string;
|
||||||
startDate?: Date;
|
referrer?: string;
|
||||||
endDate?: Date;
|
}
|
||||||
userId?: string;
|
|
||||||
}
|
export interface AnalyticsFilters {
|
||||||
export async function trackEvent(data: TrackEventData) {
|
scriptId?: string;
|
||||||
return { success: true };
|
eventType?: string;
|
||||||
}
|
startDate?: Date;
|
||||||
export async function getAnalyticsEvents(filters?: AnalyticsFilters) {
|
endDate?: Date;
|
||||||
return [];
|
userId?: string;
|
||||||
}
|
}
|
||||||
export async function getScriptAnalytics(scriptId: string, days?: number) {
|
|
||||||
return { eventCounts: [], dailyActivity: [], referrers: [], periodDays: days || 30 };
|
// Track an analytics event
|
||||||
}
|
export async function trackEvent(data: TrackEventData) {
|
||||||
export async function getPlatformAnalytics(days?: number) {
|
try {
|
||||||
return { totals: { totalScripts: 0, approvedScripts: 0, pendingScripts: 0 }, activityByType: [], popularScripts: [], dailyTrends: [], periodDays: days || 30 };
|
await db.insert(scriptAnalytics).values({
|
||||||
}
|
id: generateId(),
|
||||||
export async function getUserAnalytics(userId: string, days?: number) {
|
scriptId: data.scriptId,
|
||||||
return { userScripts: [], recentActivity: [], periodDays: days || 30 };
|
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<string>`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<number>`SUM(CASE WHEN ${scripts.isApproved} = 1 THEN 1 ELSE 0 END)`,
|
||||||
|
pendingScripts: sql<number>`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<string>`DATE(${scriptAnalytics.createdAt})`,
|
||||||
|
views: sql<number>`SUM(CASE WHEN ${scriptAnalytics.eventType} = 'view' THEN 1 ELSE 0 END)`,
|
||||||
|
downloads: sql<number>`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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,26 +1,217 @@
|
|||||||
export interface LoginCredentials {
|
import bcrypt from 'bcrypt';
|
||||||
email: string;
|
import jwt from 'jsonwebtoken';
|
||||||
password: string;
|
import { getUserByEmail, getUserByUsername, createUser } from './users';
|
||||||
}
|
import { ApiError } from './index';
|
||||||
export interface RegisterData {
|
|
||||||
email: string;
|
export interface LoginCredentials {
|
||||||
username: string;
|
email: string;
|
||||||
displayName: string;
|
password: string;
|
||||||
password: string;
|
}
|
||||||
}
|
|
||||||
export interface AuthToken {
|
export interface RegisterData {
|
||||||
token: string;
|
email: string;
|
||||||
user: any;
|
username: string;
|
||||||
}
|
displayName: string;
|
||||||
export async function login(credentials: LoginCredentials): Promise<AuthToken> {
|
password: string;
|
||||||
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<AuthToken> {
|
export interface AuthToken {
|
||||||
return { token: "demo-token", user: { id: "1", username: data.username, email: data.email, displayName: data.displayName, isAdmin: false, isModerator: false } };
|
token: string;
|
||||||
}
|
user: {
|
||||||
export async function refreshToken(token: string): Promise<AuthToken> {
|
id: string;
|
||||||
return { token: "demo-token", user: { id: "1", username: "demo", email: "demo@example.com", displayName: "Demo User", isAdmin: false, isModerator: false } };
|
email: string;
|
||||||
}
|
username: string;
|
||||||
export async function changePassword(userId: string, currentPassword: string, newPassword: string): Promise<boolean> {
|
displayName: string;
|
||||||
return true;
|
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<string> {
|
||||||
|
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<boolean> {
|
||||||
|
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<AuthToken> {
|
||||||
|
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<AuthToken> {
|
||||||
|
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<AuthToken> {
|
||||||
|
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<boolean> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,38 +1,274 @@
|
|||||||
export interface CreateCollectionData {
|
import { db } from '@/lib/db';
|
||||||
name: string;
|
import { scriptCollections, collectionScripts } from '@/lib/db/schema';
|
||||||
description?: string;
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
authorId: string;
|
import { generateId, ApiError } from './index';
|
||||||
isPublic?: boolean;
|
|
||||||
}
|
export interface CreateCollectionData {
|
||||||
export interface UpdateCollectionData {
|
name: string;
|
||||||
name?: string;
|
description?: string;
|
||||||
description?: string;
|
authorId: string;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
}
|
}
|
||||||
export async function createCollection(data: CreateCollectionData) {
|
|
||||||
return { id: "mock-collection-id", ...data, createdAt: new Date(), updatedAt: new Date() };
|
export interface UpdateCollectionData {
|
||||||
}
|
name?: string;
|
||||||
export async function getCollectionById(id: string) {
|
description?: string;
|
||||||
return null;
|
isPublic?: boolean;
|
||||||
}
|
}
|
||||||
export async function getUserCollections(userId: string) {
|
|
||||||
return [];
|
// Create a new collection
|
||||||
}
|
export async function createCollection(data: CreateCollectionData) {
|
||||||
export async function getPublicCollections(limit?: number, offset?: number) {
|
try {
|
||||||
return [];
|
const collectionId = generateId();
|
||||||
}
|
const now = new Date();
|
||||||
export async function updateCollection(id: string, data: UpdateCollectionData, userId: string) {
|
|
||||||
return { id, ...data, updatedAt: new Date() };
|
await db.insert(scriptCollections).values({
|
||||||
}
|
id: collectionId,
|
||||||
export async function deleteCollection(id: string, userId: string) {
|
name: data.name,
|
||||||
return { success: true };
|
description: data.description,
|
||||||
}
|
authorId: data.authorId,
|
||||||
export async function addScriptToCollection(collectionId: string, scriptId: string, userId: string) {
|
isPublic: data.isPublic ?? true,
|
||||||
return { id: "mock-collection-script-id", collectionId, scriptId, addedAt: new Date() };
|
createdAt: now,
|
||||||
}
|
updatedAt: now,
|
||||||
export async function removeScriptFromCollection(collectionId: string, scriptId: string, userId: string) {
|
});
|
||||||
return { success: true };
|
|
||||||
}
|
const collection = {
|
||||||
export async function isScriptInCollection(collectionId: string, scriptId: string) {
|
id: collectionId,
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from 'nanoid';
|
||||||
export const generateId = () => nanoid();
|
|
||||||
export class ApiError extends Error {
|
// Generate unique IDs
|
||||||
constructor(message: string, public status: number = 500) {
|
export const generateId = () => nanoid();
|
||||||
super(message);
|
|
||||||
this.name = "ApiError";
|
// Error handling
|
||||||
}
|
export class ApiError extends Error {
|
||||||
}
|
constructor(message: string, public status: number = 500) {
|
||||||
export * from "./scripts";
|
super(message);
|
||||||
export * from "./users";
|
this.name = 'ApiError';
|
||||||
export * from "./ratings";
|
}
|
||||||
export * from "./analytics";
|
}
|
||||||
export * from "./collections";
|
|
||||||
export * from "./auth";
|
// Export all service modules
|
||||||
|
export * from './scripts';
|
||||||
|
export * from './users';
|
||||||
|
export * from './ratings';
|
||||||
|
export * from './analytics';
|
||||||
|
export * from './collections';
|
||||||
|
export * from './auth';
|
||||||
|
45
src/lib/api/mock.ts
Normal file
45
src/lib/api/mock.ts
Normal file
@ -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(),
|
||||||
|
}),
|
||||||
|
};
|
@ -1,20 +1,191 @@
|
|||||||
export interface CreateRatingData {
|
import { db } from '@/lib/db';
|
||||||
scriptId: string;
|
import { ratings, scripts } from '@/lib/db/schema';
|
||||||
userId: string;
|
import { eq, and, avg, count } from 'drizzle-orm';
|
||||||
rating: number;
|
import { generateId, ApiError } from './index';
|
||||||
}
|
|
||||||
export async function rateScript(data: CreateRatingData) {
|
export interface CreateRatingData {
|
||||||
return { id: "mock-rating-id", ...data, createdAt: new Date(), updatedAt: new Date() };
|
scriptId: string;
|
||||||
}
|
userId: string;
|
||||||
export async function getUserRating(scriptId: string, userId: string) {
|
rating: number; // 1-5 stars
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
export async function getScriptRatings(scriptId: string) {
|
// Create or update a rating
|
||||||
return [];
|
export async function rateScript(data: CreateRatingData) {
|
||||||
}
|
try {
|
||||||
export async function getScriptRatingStats(scriptId: string) {
|
if (data.rating < 1 || data.rating > 5) {
|
||||||
return { averageRating: 0, totalRatings: 0, distribution: [] };
|
throw new ApiError('Rating must be between 1 and 5', 400);
|
||||||
}
|
}
|
||||||
export async function deleteRating(scriptId: string, userId: string) {
|
|
||||||
return { success: true };
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,51 +1,367 @@
|
|||||||
export interface ScriptFilters {
|
import { db } from '@/lib/db';
|
||||||
search?: string;
|
import { scripts, scriptVersions, ratings } from '@/lib/db/schema';
|
||||||
categories?: string[];
|
import { eq, desc, asc, and, or, like, count, sql } from 'drizzle-orm';
|
||||||
compatibleOs?: string[];
|
import { generateId, ApiError } from './index';
|
||||||
sortBy?: string;
|
|
||||||
limit?: number;
|
export interface CreateScriptData {
|
||||||
isApproved?: boolean;
|
name: string;
|
||||||
}
|
description: string;
|
||||||
export interface UpdateScriptData {
|
content: string;
|
||||||
name?: string;
|
compatibleOs: string[];
|
||||||
description?: string;
|
categories: string[];
|
||||||
content?: string;
|
tags?: string[];
|
||||||
}
|
gitRepositoryUrl?: string;
|
||||||
export interface CreateScriptData {
|
authorId: string;
|
||||||
name: string;
|
authorName: string;
|
||||||
description: string;
|
version?: string;
|
||||||
content: string;
|
}
|
||||||
categories: string[];
|
|
||||||
compatibleOs: string[];
|
export interface UpdateScriptData {
|
||||||
tags?: string[];
|
name?: string;
|
||||||
}
|
description?: string;
|
||||||
export async function getScripts(filters?: ScriptFilters) {
|
content?: string;
|
||||||
return { scripts: [], total: 0 };
|
compatibleOs?: string[];
|
||||||
}
|
categories?: string[];
|
||||||
export async function getScriptById(id: string) {
|
tags?: string[];
|
||||||
return null;
|
gitRepositoryUrl?: string;
|
||||||
}
|
version?: string;
|
||||||
export async function getPopularScripts() {
|
}
|
||||||
return [];
|
|
||||||
}
|
export interface ScriptFilters {
|
||||||
export async function getRecentScripts() {
|
categories?: string[];
|
||||||
return [];
|
compatibleOs?: string[];
|
||||||
}
|
search?: string;
|
||||||
export async function createScript(data: CreateScriptData, userId: string) {
|
authorId?: string;
|
||||||
return { id: "mock-script-id", ...data, authorId: userId };
|
isApproved?: boolean;
|
||||||
}
|
sortBy?: 'newest' | 'oldest' | 'popular' | 'rating';
|
||||||
export async function updateScript(id: string, data: UpdateScriptData, userId: string) {
|
limit?: number;
|
||||||
return { id, ...data };
|
offset?: number;
|
||||||
}
|
}
|
||||||
export async function deleteScript(id: string, userId: string) {
|
|
||||||
return { success: true };
|
// Create a new script
|
||||||
}
|
export async function createScript(data: CreateScriptData) {
|
||||||
export async function moderateScript(id: string, isApproved: boolean, moderatorId: string) {
|
try {
|
||||||
return { id, isApproved };
|
const scriptId = generateId();
|
||||||
}
|
const now = new Date();
|
||||||
export async function incrementViewCount(id: string) {
|
|
||||||
return { success: true };
|
await db.insert(scripts).values({
|
||||||
}
|
id: scriptId,
|
||||||
export async function incrementDownloadCount(id: string) {
|
name: data.name,
|
||||||
return { success: true };
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,37 +1,174 @@
|
|||||||
export interface CreateUserData {
|
import { db } from '@/lib/db';
|
||||||
email: string;
|
import { users } from '@/lib/db/schema';
|
||||||
username: string;
|
import { eq, like } from 'drizzle-orm';
|
||||||
displayName: string;
|
import { generateId, ApiError } from './index';
|
||||||
avatarUrl?: string;
|
|
||||||
bio?: string;
|
export interface CreateUserData {
|
||||||
}
|
email: string;
|
||||||
export interface UpdateUserData {
|
username: string;
|
||||||
username?: string;
|
displayName: string;
|
||||||
displayName?: string;
|
avatarUrl?: string;
|
||||||
avatarUrl?: string;
|
bio?: string;
|
||||||
bio?: string;
|
}
|
||||||
}
|
|
||||||
export async function createUser(data: CreateUserData) {
|
export interface UpdateUserData {
|
||||||
return { id: "mock-user-id", ...data, isAdmin: false, isModerator: false, createdAt: new Date(), updatedAt: new Date() };
|
username?: string;
|
||||||
}
|
displayName?: string;
|
||||||
export async function getUserById(id: string) {
|
avatarUrl?: string;
|
||||||
return null;
|
bio?: string;
|
||||||
}
|
}
|
||||||
export async function getUserByEmail(email: string) {
|
|
||||||
return null;
|
// Create a new user
|
||||||
}
|
export async function createUser(data: CreateUserData) {
|
||||||
export async function getUserByUsername(username: string) {
|
try {
|
||||||
return null;
|
const userId = generateId();
|
||||||
}
|
const now = new Date();
|
||||||
export async function updateUser(id: string, data: UpdateUserData) {
|
|
||||||
return { id, ...data, updatedAt: new Date() };
|
const userData = {
|
||||||
}
|
id: userId,
|
||||||
export async function updateUserPermissions(id: string, permissions: any) {
|
email: data.email,
|
||||||
return { id, ...permissions, updatedAt: new Date() };
|
username: data.username,
|
||||||
}
|
displayName: data.displayName,
|
||||||
export async function searchUsers(query: string, limit?: number) {
|
avatarUrl: data.avatarUrl || null,
|
||||||
return [];
|
bio: data.bio || null,
|
||||||
}
|
isAdmin: false,
|
||||||
export async function getAllUsers(limit?: number, offset?: number) {
|
isModerator: false,
|
||||||
return [];
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
33
src/lib/db/browser.ts
Normal file
33
src/lib/db/browser.ts
Normal file
@ -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 = {};
|
@ -1 +1,88 @@
|
|||||||
export const db = {};
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1 +1,186 @@
|
|||||||
export const users = {}; export const scripts = {}; export const ratings = {}; export const scriptVersions = {}; export const scriptAnalytics = {}; export const scriptCollections = {}; export const collectionScripts = {};
|
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<string[]>().notNull(),
|
||||||
|
categories: json('categories').$type<string[]>().notNull(),
|
||||||
|
tags: json('tags').$type<string[]>(),
|
||||||
|
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],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
Reference in New Issue
Block a user