diff --git a/.gitignore b/.gitignore index 97088e4..78c9d81 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,139 @@ Thumbs.db # Git-specific *.orig -# Ignore plugin build outputs if copied manually -assets/plugins/*.wasm +# Environment and sensitive files +.env +.env.local +.env.development +.env.test +.env.production +*.key +*.pem +*.p12 +*.pfx +secrets.json +config.json + +# Test files +test_*.rs +test_*.js +test_*.py +test_*.ts +test_*.tsx +*_test.rs +*_test.js +*_test.py +*_test.ts +*_test.tsx +*.test.rs +*.test.js +*.test.py +*.test.ts +*.test.tsx +*.spec.rs +*.spec.js +*.spec.py +*.spec.ts +*.spec.tsx + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Build outputs +dist/ +build/ +out/ +.next/ +.nuxt/ +.vuepress/dist + +# Coverage reports +coverage/ +*.lcov +.nyc_output + +# Temporary files +*.tmp +*.temp +.cache/ +.parcel-cache/ + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Backup files +*.bak +*.backup +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Rust specific +**/target/ +Cargo.lock + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json diff --git a/sandcrate-backend/Cargo.toml b/sandcrate-backend/Cargo.toml index 4e2352a..e3c43a7 100644 --- a/sandcrate-backend/Cargo.toml +++ b/sandcrate-backend/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "sandcrate-backend" edition = "2021" +default-run = "sandcrate-backend" [dependencies] -axum = { version = "0.7", features = ["ws"] } +axum = { version = "0.7", features = ["ws", "multipart"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -17,4 +18,7 @@ tower-http = { version = "0.5", features = ["cors"] } axum-extra = { version = "0.9", features = ["typed-header"] } tokio-tungstenite = "0.21" futures-util = { version = "0.3", features = ["sink"] } -uuid = { version = "1.0", features = ["v4"] } \ No newline at end of file +uuid = { version = "1.0", features = ["v4"] } +sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "migrate"] } +dotenv = "0.15" +async-trait = "0.1" \ No newline at end of file diff --git a/sandcrate-backend/env.example b/sandcrate-backend/env.example new file mode 100644 index 0000000..279b4be --- /dev/null +++ b/sandcrate-backend/env.example @@ -0,0 +1,17 @@ +# Database Configuration +DATABASE_URL=postgresql://sandcrate:sandcrate@localhost:5432/sandcrate + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-in-production +JWT_EXPIRATION_HOURS=24 + +# Server Configuration +SERVER_HOST=127.0.0.1 +SERVER_PORT=3000 + +# Plugin Configuration +PLUGINS_DIR=../assets/plugins +MAX_PLUGIN_SIZE_MB=50 + +# Logging +LOG_LEVEL=info \ No newline at end of file diff --git a/sandcrate-backend/migrations/20240101000001_create_initial_schema.sql b/sandcrate-backend/migrations/20240101000001_create_initial_schema.sql new file mode 100644 index 0000000..67e27d1 --- /dev/null +++ b/sandcrate-backend/migrations/20240101000001_create_initial_schema.sql @@ -0,0 +1,82 @@ +-- Create custom types +CREATE TYPE plugin_status AS ENUM ('active', 'inactive', 'error', 'processing'); +CREATE TYPE execution_status AS ENUM ('running', 'completed', 'failed', 'cancelled'); +CREATE TYPE user_role AS ENUM ('admin', 'user', 'guest'); + +-- Create users table +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE, + name VARCHAR(255) NOT NULL, + role user_role NOT NULL DEFAULT 'user', + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + last_login_at TIMESTAMP WITH TIME ZONE +); + +-- Create plugins table +CREATE TABLE plugins ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + filename VARCHAR(255) NOT NULL UNIQUE, + file_path TEXT NOT NULL, + file_size BIGINT NOT NULL, + description TEXT, + version VARCHAR(50) NOT NULL DEFAULT '1.0.0', + author VARCHAR(255), + tags TEXT[] DEFAULT '{}', + status plugin_status NOT NULL DEFAULT 'active', + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + last_executed_at TIMESTAMP WITH TIME ZONE, + execution_count INTEGER NOT NULL DEFAULT 0, + average_execution_time_ms BIGINT +); + +-- Create plugin_executions table +CREATE TABLE plugin_executions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + plugin_id UUID NOT NULL REFERENCES plugins(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + session_id VARCHAR(255), + parameters JSONB, + result TEXT, + error TEXT, + execution_time_ms BIGINT NOT NULL DEFAULT 0, + status execution_status NOT NULL DEFAULT 'running', + started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + completed_at TIMESTAMP WITH TIME ZONE +); + +-- Create indexes for better performance +CREATE INDEX idx_plugins_status ON plugins(status); +CREATE INDEX idx_plugins_created_at ON plugins(created_at DESC); +CREATE INDEX idx_plugins_filename ON plugins(filename); +CREATE INDEX idx_plugin_executions_plugin_id ON plugin_executions(plugin_id); +CREATE INDEX idx_plugin_executions_started_at ON plugin_executions(started_at DESC); +CREATE INDEX idx_plugin_executions_user_id ON plugin_executions(user_id); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); + +-- Create function to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- Create triggers to automatically update updated_at +CREATE TRIGGER update_plugins_updated_at BEFORE UPDATE ON plugins + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- Insert default admin user +INSERT INTO users (username, name, role, is_active) +VALUES ('admin', 'System Administrator', 'admin', true) +ON CONFLICT (username) DO NOTHING; \ No newline at end of file diff --git a/sandcrate-backend/scripts/init_db.rs b/sandcrate-backend/scripts/init_db.rs new file mode 100644 index 0000000..22b88b7 --- /dev/null +++ b/sandcrate-backend/scripts/init_db.rs @@ -0,0 +1,51 @@ +use std::env; +use dotenv::dotenv; + +use sandcrate_backend::{ + DatabaseConfig, create_pool, PostgresPluginRepository, PluginService +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv().ok(); + + println!("Initializing Sandcrate database..."); + + let db_config = DatabaseConfig::default(); + let db_pool = create_pool(&db_config).await?; + + println!("Database connection established"); + + let plugin_repo = PostgresPluginRepository::new(db_pool.clone()); + let plugin_service = PluginService::new(Box::new(plugin_repo)); + + let plugins_dir = env::var("PLUGINS_DIR").unwrap_or_else(|_| "../assets/plugins".to_string()); + println!("Syncing plugins from: {}", plugins_dir); + + match plugin_service.sync_plugins_from_filesystem(&plugins_dir).await { + Ok(plugins) => { + println!("Synced {} plugins from filesystem", plugins.len()); + for plugin in plugins { + println!(" - {} (v{})", plugin.name, plugin.version); + } + } + Err(e) => { + println!("Failed to sync plugins: {}", e); + } + } + + println!("\nAll plugins in database:"); + match plugin_service.list_plugins(None, None).await { + Ok(plugins) => { + for plugin in plugins { + println!(" - {} (v{}) - {}", plugin.name, plugin.version, plugin.status); + } + } + Err(e) => { + println!("Failed to list plugins: {}", e); + } + } + + println!("\nDatabase initialization completed!"); + Ok(()) +} \ No newline at end of file diff --git a/sandcrate-backend/scripts/setup_db.sh b/sandcrate-backend/scripts/setup_db.sh new file mode 100644 index 0000000..815eb38 --- /dev/null +++ b/sandcrate-backend/scripts/setup_db.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Database setup script for Sandcrate +set -e + +echo "🚀 Setting up PostgreSQL database for Sandcrate..." + +# Check if PostgreSQL is installed +if ! command -v psql &> /dev/null; then + echo "❌ PostgreSQL is not installed. Please install PostgreSQL first." + echo " Ubuntu/Debian: sudo apt-get install postgresql postgresql-contrib" + echo " macOS: brew install postgresql" + echo " Windows: Download from https://www.postgresql.org/download/windows/" + exit 1 +fi + +# Check if PostgreSQL service is running +if ! pg_isready -q; then + echo "❌ PostgreSQL service is not running. Please start PostgreSQL first." + echo " Ubuntu/Debian: sudo systemctl start postgresql" + echo " macOS: brew services start postgresql" + exit 1 +fi + +# Create database and user +echo "📦 Creating database and user..." + +# Create user (if it doesn't exist) +psql -U postgres -c "CREATE USER sandcrate WITH PASSWORD 'sandcrate';" 2>/dev/null || echo "User sandcrate already exists" + +# Create database (if it doesn't exist) +psql -U postgres -c "CREATE DATABASE sandcrate OWNER sandcrate;" 2>/dev/null || echo "Database sandcrate already exists" + +# Grant privileges +psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE sandcrate TO sandcrate;" + +echo "✅ Database setup completed!" +echo "" +echo "📋 Next steps:" +echo "1. Copy env.example to .env and update the DATABASE_URL if needed" +echo "2. Run migrations: cargo sqlx migrate run" +echo "3. Start the backend: cargo run" +echo "" +echo "🔗 Database connection:" +echo " Host: localhost" +echo " Port: 5432" +echo " Database: sandcrate" +echo " User: sandcrate" +echo " Password: sandcrate" \ No newline at end of file diff --git a/sandcrate-backend/src/api.rs b/sandcrate-backend/src/api.rs index a0c77f7..de2195b 100644 --- a/sandcrate-backend/src/api.rs +++ b/sandcrate-backend/src/api.rs @@ -1,15 +1,19 @@ use axum::{ - routing::{get, post}, - Json, Router, extract::{State, Path}, + routing::{get, post, delete}, + Json, Router, extract::{State, Path, Multipart}, http::StatusCode, response::{IntoResponse, Response}, }; -use serde::Serialize; +use axum_extra::{ + extract::TypedHeader, + headers::{authorization::Bearer, Authorization}, +}; +use serde::{Serialize, Deserialize}; use std::sync::Arc; use std::fs; use std::path::Path as FsPath; -use crate::auth::AuthConfig; +use crate::auth::{AuthConfig, validate_token}; use crate::plugin; #[derive(Serialize)] @@ -27,7 +31,7 @@ struct PluginList { plugins: Vec, } -#[derive(Serialize)] +#[derive(Deserialize)] struct PluginExecutionRequest { parameters: Option, timeout: Option, @@ -48,17 +52,24 @@ struct ApiResponse { error: Option, } -#[derive(Serialize)] -struct ErrorResponse { - success: bool, - error: String, - code: String, -} + async fn get_plugins( - State(_config): State>, -) -> Json> { - let plugins_dir = FsPath::new("assets/plugins"); + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, +) -> Result>, (StatusCode, Json>)> { + let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(ApiResponse { + success: false, + data: None, + error: Some("Invalid or missing authentication token".to_string()), + }) + ) + })?; + let plugins_dir = FsPath::new("../assets/plugins"); let mut plugins = Vec::new(); if let Ok(entries) = fs::read_dir(plugins_dir) { @@ -102,29 +113,41 @@ async fn get_plugins( } } - Json(ApiResponse { + Ok(Json(ApiResponse { success: true, data: Some(PluginList { plugins }), error: None, - }) + })) } async fn get_plugin( - State(_config): State>, + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, Path(plugin_id): Path, -) -> Response { - let plugins_dir = FsPath::new("assets/plugins"); +) -> Result>)> { + let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(ApiResponse { + success: false, + data: None, + error: Some("Invalid or missing authentication token".to_string()), + }) + ) + })?; + let plugins_dir = FsPath::new("../assets/plugins"); let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id)); if !plugin_path.exists() { - return ( + return Ok(( StatusCode::NOT_FOUND, Json(ApiResponse:: { success: false, data: None, error: Some(format!("Plugin '{}' not found", plugin_id)), }) - ).into_response(); + ).into_response()); } if let Ok(metadata) = fs::metadata(&plugin_path) { @@ -156,53 +179,62 @@ async fn get_plugin( status: "ready".to_string(), }; - ( + Ok(( StatusCode::OK, Json(ApiResponse { success: true, data: Some(plugin), error: None, }) - ).into_response() + ).into_response()) } else { - ( + Ok(( StatusCode::INTERNAL_SERVER_ERROR, Json(ApiResponse:: { success: false, data: None, error: Some("Failed to read plugin metadata".to_string()), }) - ).into_response() + ).into_response()) } } async fn execute_plugin( - State(_config): State>, + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, Path(plugin_id): Path, - Json(request): Json, -) -> Response { + Json(request): Json, +) -> Result>)> { + let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(ApiResponse { + success: false, + data: None, + error: Some("Invalid or missing authentication token".to_string()), + }) + ) + })?; let start_time = std::time::Instant::now(); - // Find the plugin file - let plugins_dir = FsPath::new("assets/plugins"); + let plugins_dir = FsPath::new("../assets/plugins"); let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id)); if !plugin_path.exists() { - return ( + return Ok(( StatusCode::NOT_FOUND, Json(ApiResponse:: { success: false, data: None, error: Some(format!("Plugin '{}' not found", plugin_id)), }) - ).into_response(); + ).into_response()); } - // Extract parameters from the request - let parameters = request.get("parameters").cloned(); - let timeout = request.get("timeout").and_then(|v| v.as_u64()); + let parameters = request.parameters; + let timeout = request.timeout; - // Execute the plugin with parameters and timeout let plugin_path_str = plugin_path.to_str().unwrap_or(""); let execution_result = plugin::run_plugin_with_params( plugin_path_str, @@ -213,47 +245,170 @@ async fn execute_plugin( let execution_time = start_time.elapsed(); let execution_time_ms = execution_time.as_millis() as u64; - match execution_result { - Ok(result) => { - let response = PluginExecutionResponse { - success: true, - result, - execution_time_ms, - error: None, - }; - - ( - StatusCode::OK, - Json(ApiResponse { + match execution_result { + Ok(result) => { + let response = PluginExecutionResponse { success: true, - data: Some(response), + result, + execution_time_ms, error: None, - }) - ).into_response() + }; + + Ok(( + StatusCode::OK, + Json(ApiResponse { + success: true, + data: Some(response), + error: None, + }) + ).into_response()) + } + Err(e) => { + let response = PluginExecutionResponse { + success: false, + result: String::new(), + execution_time_ms, + error: Some(e.to_string()), + }; + + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse { + success: true, + data: Some(response), + error: None, + }) + ).into_response()) + } } - Err(e) => { - let response = PluginExecutionResponse { +} + +async fn upload_plugin( + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, + mut multipart: Multipart, +) -> Result>, (StatusCode, Json>)> { + let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(ApiResponse { + success: false, + data: None, + error: Some("Invalid or missing authentication token".to_string()), + }) + ) + })?; + + while let Some(field) = multipart.next_field().await.map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(ApiResponse { success: false, - result: String::new(), - execution_time_ms, - error: Some(e.to_string()), - }; + data: None, + error: Some("Failed to read multipart data".to_string()), + }) + ) + })? { + let name = field.name().unwrap_or("").to_string(); + if name == "plugin" { + let data = field.bytes().await.map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(ApiResponse { + success: false, + data: None, + error: Some("Failed to read plugin file".to_string()), + }) + ) + })?; + let filename = format!("plugin_{}.wasm", uuid::Uuid::new_v4()); + let plugin_path = FsPath::new("../assets/plugins").join(&filename); + + if let Err(_) = fs::write(&plugin_path, data) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse { + success: false, + data: None, + error: Some("Failed to save plugin file".to_string()), + }) + )); + } + + return Ok(Json(ApiResponse { + success: true, + data: Some(filename), + error: None, + })); + } + } + + Err(( + StatusCode::BAD_REQUEST, + Json(ApiResponse { + success: false, + data: None, + error: Some("No plugin file found in request".to_string()), + }) + )) +} + +async fn delete_plugin( + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, + Path(plugin_id): Path, +) -> Result>, (StatusCode, Json>)> { + let _user = validate_token(State(config.clone()), TypedHeader(Authorization(bearer))).await + .map_err(|_| { ( - StatusCode::INTERNAL_SERVER_ERROR, + StatusCode::UNAUTHORIZED, Json(ApiResponse { - success: true, - data: Some(response), - error: None, + success: false, + data: None, + error: Some("Invalid or missing authentication token".to_string()), }) - ).into_response() - } + ) + })?; + + let plugins_dir = FsPath::new("../assets/plugins"); + let plugin_path = plugins_dir.join(format!("{}.wasm", plugin_id)); + + if !plugin_path.exists() { + return Err(( + StatusCode::NOT_FOUND, + Json(ApiResponse { + success: false, + data: None, + error: Some(format!("Plugin '{}' not found", plugin_id)), + }) + )); } + + if let Err(_) = fs::remove_file(&plugin_path) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiResponse { + success: false, + data: None, + error: Some("Failed to delete plugin file".to_string()), + }) + )); + } + + Ok(Json(ApiResponse { + success: true, + data: Some(format!("Plugin '{}' deleted successfully", plugin_id)), + error: None, + })) } pub fn routes() -> Router> { Router::new() .route("/plugins", get(get_plugins)) + .route("/plugins/upload", post(upload_plugin)) .route("/plugins/:id", get(get_plugin)) + .route("/plugins/:id", delete(delete_plugin)) .route("/plugins/:id/execute", post(execute_plugin)) } diff --git a/sandcrate-backend/src/auth.rs b/sandcrate-backend/src/auth.rs index 5bd10b9..09319ea 100644 --- a/sandcrate-backend/src/auth.rs +++ b/sandcrate-backend/src/auth.rs @@ -90,7 +90,6 @@ pub async fn login( Json(payload): Json, ) -> Result, (StatusCode, Json)> { println!("Login attempt for user: {}", payload.username); - // Authenticate using PAM let mut authenticator = Authenticator::with_password("login") .map_err(|_| { ( @@ -107,10 +106,8 @@ pub async fn login( match authenticator.authenticate() { Ok(_) => { - // Check user privileges let (is_admin, role) = check_user_privileges(&payload.username); - // Get user's real name from system let real_name = std::process::Command::new("getent") .args(["passwd", &payload.username]) .output() @@ -125,7 +122,6 @@ pub async fn login( }) .unwrap_or_else(|| payload.username.clone()); - // Generate JWT token let now = Utc::now(); let expires_at = now + Duration::hours(24); diff --git a/sandcrate-backend/src/bin/execute_plugin.rs b/sandcrate-backend/src/bin/execute_plugin.rs index 6600080..5d00ab8 100644 --- a/sandcrate-backend/src/bin/execute_plugin.rs +++ b/sandcrate-backend/src/bin/execute_plugin.rs @@ -1,5 +1,4 @@ use std::fs; -use std::path::Path; use wasmtime::*; use wasmtime_wasi::WasiCtxBuilder; use std::env; @@ -13,10 +12,8 @@ fn main() -> Result<(), Box> { let plugin_path = &args[1]; - // Create a WASM engine let engine = Engine::default(); - // Create a store with WASI context let wasi = WasiCtxBuilder::new() .inherit_stdio() .inherit_args()? @@ -24,18 +21,14 @@ fn main() -> Result<(), Box> { let mut store = Store::new(&engine, wasi); - // Read the WASM module let wasm_bytes = fs::read(plugin_path)?; let module = Module::new(&engine, &wasm_bytes)?; - // Create a linker and add WASI let mut linker = Linker::new(&engine); wasmtime_wasi::add_to_linker(&mut linker, |s| s)?; - // Instantiate the module let instance = linker.instantiate(&mut store, &module)?; - // Try to find and call a suitable function let function_names = ["_start", "start", "main", "run"]; let mut executed = false; diff --git a/sandcrate-backend/src/database.rs b/sandcrate-backend/src/database.rs new file mode 100644 index 0000000..0469529 --- /dev/null +++ b/sandcrate-backend/src/database.rs @@ -0,0 +1,331 @@ +use sqlx::{PgPool, PgPoolOptions, postgres::PgPoolOptions as PgPoolOptionsPostgres}; +use std::time::Duration; +use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; +use uuid::Uuid; + +#[derive(Debug, Clone)] +pub struct DatabaseConfig { + pub url: String, + pub max_connections: u32, + pub min_connections: u32, + pub connect_timeout: Duration, + pub idle_timeout: Duration, + pub max_lifetime: Duration, +} + +impl Default for DatabaseConfig { + fn default() -> Self { + Self { + url: std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgresql://sandcrate:sandcrate@localhost:5432/sandcrate".to_string()), + max_connections: 10, + min_connections: 2, + connect_timeout: Duration::from_secs(30), + idle_timeout: Duration::from_secs(300), + max_lifetime: Duration::from_secs(3600), + } + } +} + +pub async fn create_pool(config: &DatabaseConfig) -> Result { + PgPoolOptions::new() + .max_connections(config.max_connections) + .min_connections(config.min_connections) + .connect_timeout(config.connect_timeout) + .idle_timeout(config.idle_timeout) + .max_lifetime(config.max_lifetime) + .connect(&config.url) + .await +} + + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct Plugin { + pub id: Uuid, + pub name: String, + pub filename: String, + pub file_path: String, + pub file_size: i64, + pub description: Option, + pub version: String, + pub author: Option, + pub tags: Vec, + pub status: PluginStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + pub last_executed_at: Option>, + pub execution_count: i32, + pub average_execution_time_ms: Option, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "plugin_status", rename_all = "lowercase")] +pub enum PluginStatus { + Active, + Inactive, + Error, + Processing, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct PluginExecution { + pub id: Uuid, + pub plugin_id: Uuid, + pub user_id: Option, + pub session_id: Option, + pub parameters: Option, + pub result: Option, + pub error: Option, + pub execution_time_ms: i64, + pub status: ExecutionStatus, + pub started_at: DateTime, + pub completed_at: Option>, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "execution_status", rename_all = "lowercase")] +pub enum ExecutionStatus { + Running, + Completed, + Failed, + Cancelled, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct User { + pub id: Uuid, + pub username: String, + pub email: Option, + pub name: String, + pub role: UserRole, + pub is_active: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub last_login_at: Option>, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "user_role", rename_all = "lowercase")] +pub enum UserRole { + Admin, + User, + Guest, +} + +#[async_trait::async_trait] +pub trait PluginRepository { + async fn create_plugin(&self, plugin: CreatePluginRequest) -> Result; + async fn get_plugin_by_id(&self, id: Uuid) -> Result, sqlx::Error>; + async fn get_plugin_by_filename(&self, filename: &str) -> Result, sqlx::Error>; + async fn list_plugins(&self, limit: Option, offset: Option) -> Result, sqlx::Error>; + async fn update_plugin(&self, id: Uuid, updates: UpdatePluginRequest) -> Result; + async fn delete_plugin(&self, id: Uuid) -> Result; + async fn record_execution(&self, execution: CreateExecutionRequest) -> Result; + async fn get_execution_history(&self, plugin_id: Uuid, limit: Option) -> Result, sqlx::Error>; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreatePluginRequest { + pub name: String, + pub filename: String, + pub file_path: String, + pub file_size: i64, + pub description: Option, + pub version: String, + pub author: Option, + pub tags: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdatePluginRequest { + pub name: Option, + pub description: Option, + pub version: Option, + pub author: Option, + pub tags: Option>, + pub status: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateExecutionRequest { + pub plugin_id: Uuid, + pub user_id: Option, + pub session_id: Option, + pub parameters: Option, +} + +pub struct PostgresPluginRepository { + pool: PgPool, +} + +impl PostgresPluginRepository { + pub fn new(pool: PgPool) -> Self { + Self { pool } + } +} + +#[async_trait::async_trait] +impl PluginRepository for PostgresPluginRepository { + async fn create_plugin(&self, plugin: CreatePluginRequest) -> Result { + let id = Uuid::new_v4(); + let now = Utc::now(); + + sqlx::query_as!( + Plugin, + r#" + INSERT INTO plugins (id, name, filename, file_path, file_size, description, version, author, tags, status, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING * + "#, + id, + plugin.name, + plugin.filename, + plugin.file_path, + plugin.file_size, + plugin.description, + plugin.version, + plugin.author, + &plugin.tags, + PluginStatus::Active as PluginStatus, + now, + now + ) + .fetch_one(&self.pool) + .await + } + + async fn get_plugin_by_id(&self, id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as!( + Plugin, + "SELECT * FROM plugins WHERE id = $1", + id + ) + .fetch_optional(&self.pool) + .await + } + + async fn get_plugin_by_filename(&self, filename: &str) -> Result, sqlx::Error> { + sqlx::query_as!( + Plugin, + "SELECT * FROM plugins WHERE filename = $1", + filename + ) + .fetch_optional(&self.pool) + .await + } + + async fn list_plugins(&self, limit: Option, offset: Option) -> Result, sqlx::Error> { + let limit = limit.unwrap_or(100); + let offset = offset.unwrap_or(0); + + sqlx::query_as!( + Plugin, + "SELECT * FROM plugins ORDER BY created_at DESC LIMIT $1 OFFSET $2", + limit, + offset + ) + .fetch_all(&self.pool) + .await + } + + async fn update_plugin(&self, id: Uuid, updates: UpdatePluginRequest) -> Result { + let now = Utc::now(); + + let mut query = String::from("UPDATE plugins SET updated_at = $1"); + let mut params: Vec + Send + Sync>> = vec![Box::new(now)]; + let mut param_count = 1; + + if let Some(name) = updates.name { + param_count += 1; + query.push_str(&format!(", name = ${}", param_count)); + params.push(Box::new(name)); + } + + if let Some(description) = updates.description { + param_count += 1; + query.push_str(&format!(", description = ${}", param_count)); + params.push(Box::new(description)); + } + + if let Some(version) = updates.version { + param_count += 1; + query.push_str(&format!(", version = ${}", param_count)); + params.push(Box::new(version)); + } + + if let Some(author) = updates.author { + param_count += 1; + query.push_str(&format!(", author = ${}", param_count)); + params.push(Box::new(author)); + } + + if let Some(tags) = updates.tags { + param_count += 1; + query.push_str(&format!(", tags = ${}", param_count)); + params.push(Box::new(tags)); + } + + if let Some(status) = updates.status { + param_count += 1; + query.push_str(&format!(", status = ${}", param_count)); + params.push(Box::new(status)); + } + + param_count += 1; + query.push_str(&format!(" WHERE id = ${} RETURNING *", param_count)); + params.push(Box::new(id)); + + sqlx::query_as::<_, Plugin>(&query) + .bind_all(params) + .fetch_one(&self.pool) + .await + } + + async fn delete_plugin(&self, id: Uuid) -> Result { + let result = sqlx::query!( + "DELETE FROM plugins WHERE id = $1", + id + ) + .execute(&self.pool) + .await?; + + Ok(result.rows_affected() > 0) + } + + async fn record_execution(&self, execution: CreateExecutionRequest) -> Result { + let id = Uuid::new_v4(); + let now = Utc::now(); + + sqlx::query_as!( + PluginExecution, + r#" + INSERT INTO plugin_executions (id, plugin_id, user_id, session_id, parameters, status, started_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + "#, + id, + execution.plugin_id, + execution.user_id, + execution.session_id, + execution.parameters, + ExecutionStatus::Running as ExecutionStatus, + now + ) + .fetch_one(&self.pool) + .await + } + + async fn get_execution_history(&self, plugin_id: Uuid, limit: Option) -> Result, sqlx::Error> { + let limit = limit.unwrap_or(50); + + sqlx::query_as!( + PluginExecution, + "SELECT * FROM plugin_executions WHERE plugin_id = $1 ORDER BY started_at DESC LIMIT $2", + plugin_id, + limit + ) + .fetch_all(&self.pool) + .await + } +} \ No newline at end of file diff --git a/sandcrate-backend/src/lib.rs b/sandcrate-backend/src/lib.rs index 2c1dd4c..67f5d79 100644 --- a/sandcrate-backend/src/lib.rs +++ b/sandcrate-backend/src/lib.rs @@ -2,6 +2,8 @@ mod api; mod auth; pub mod plugin; mod websocket; +mod database; +mod services; use std::net::SocketAddr; use std::sync::Arc; @@ -11,13 +13,22 @@ use axum::{Router, routing::get}; pub use plugin::run_plugin; pub use websocket::{WebSocketManager, PluginExecutionSession}; +pub use database::{DatabaseConfig, create_pool, PostgresPluginRepository, PluginRepository}; +pub use services::PluginService; #[tokio::main] pub async fn run_backend() { + dotenv::dotenv().ok(); + + let db_config = DatabaseConfig::default(); + let db_pool = create_pool(&db_config).await.expect("Failed to create database pool"); + let plugin_repo = Arc::new(PostgresPluginRepository::new(db_pool.clone())); + let plugin_service = Arc::new(PluginService::new(plugin_repo.clone())); + let auth_config = Arc::new(auth::AuthConfig::new()); let ws_manager = Arc::new(websocket::WebSocketManager::new()); - let api_router = api::routes().with_state(auth_config.clone()); + let api_router = api::routes().with_state((auth_config.clone(), plugin_service.clone())); let auth_router = auth::auth_routes().with_state(auth_config.clone()); let ws_router = Router::new() .route("/plugins", get(websocket::plugin_execution_websocket)) @@ -30,12 +41,6 @@ pub async fn run_backend() { .layer(CorsLayer::permissive()); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - println!("Backend running at http://{}", addr); - println!("API endpoints:"); - println!(" GET http://{}/api/plugins", addr); - println!(" POST http://{}/auth/login", addr); - println!("WebSocket endpoints:"); - println!(" WS ws://{}/ws/plugins", addr); let listener = TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app.into_make_service()) diff --git a/sandcrate-backend/src/plugin.rs b/sandcrate-backend/src/plugin.rs index ed2b087..055c2a9 100644 --- a/sandcrate-backend/src/plugin.rs +++ b/sandcrate-backend/src/plugin.rs @@ -4,14 +4,9 @@ use wasmtime::*; use wasmtime_wasi::WasiCtxBuilder; use serde_json::Value; use tokio::sync::broadcast; -use std::sync::Arc; -use tokio::sync::Mutex; -use std::io::{Read, Write}; - - pub fn list_plugins() -> Vec { - let plugins_dir = Path::new("assets/plugins"); + let plugins_dir = Path::new("../assets/plugins"); if !plugins_dir.exists() { return vec![]; @@ -38,7 +33,7 @@ pub fn run_plugin(plugin_path: &str) -> Result, + _parameters: Option, _timeout: Option ) -> Result> { let engine = Engine::default(); diff --git a/sandcrate-backend/src/services.rs b/sandcrate-backend/src/services.rs new file mode 100644 index 0000000..65ff286 --- /dev/null +++ b/sandcrate-backend/src/services.rs @@ -0,0 +1,117 @@ +use std::sync::Arc; +use uuid::Uuid; +use chrono::Utc; +use serde_json::Value; + +use crate::database::{ + PluginRepository, PostgresPluginRepository, CreatePluginRequest, UpdatePluginRequest, + CreateExecutionRequest, Plugin, PluginExecution, PluginStatus, ExecutionStatus +}; + +pub struct PluginService { + repo: Arc, +} + +impl PluginService { + pub fn new(repo: Arc) -> Self { + Self { repo } + } + + pub async fn list_plugins(&self, limit: Option, offset: Option) -> Result, Box> { + self.repo.list_plugins(limit, offset).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn get_plugin_by_id(&self, id: Uuid) -> Result, Box> { + self.repo.get_plugin_by_id(id).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn get_plugin_by_filename(&self, filename: &str) -> Result, Box> { + self.repo.get_plugin_by_filename(filename).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn create_plugin(&self, request: CreatePluginRequest) -> Result> { + self.repo.create_plugin(request).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn update_plugin(&self, id: Uuid, request: UpdatePluginRequest) -> Result> { + self.repo.update_plugin(id, request).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn delete_plugin(&self, id: Uuid) -> Result> { + self.repo.delete_plugin(id).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn record_execution_start(&self, plugin_id: Uuid, user_id: Option, session_id: Option, parameters: Option) -> Result> { + let request = CreateExecutionRequest { + plugin_id, + user_id, + session_id, + parameters, + }; + + self.repo.record_execution(request).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn get_execution_history(&self, plugin_id: Uuid, limit: Option) -> Result, Box> { + self.repo.get_execution_history(plugin_id, limit).await + .map_err(|e| Box::new(e) as Box) + } + + pub async fn sync_plugins_from_filesystem(&self, plugins_dir: &str) -> Result, Box> { + use std::fs; + use std::path::Path; + + let mut synced_plugins = Vec::new(); + let plugins_path = Path::new(plugins_dir); + + if !plugins_path.exists() { + return Ok(synced_plugins); + } + + for entry in fs::read_dir(plugins_path)? { + let entry = entry?; + let path = entry.path(); + + if let Some(extension) = path.extension() { + if extension == "wasm" { + let filename = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + if let Ok(Some(_)) = self.get_plugin_by_filename(&filename).await { + continue; + } + + let metadata = fs::metadata(&path)?; + let name = filename.replace(".wasm", ""); + + let request = CreatePluginRequest { + name: name.clone(), + filename, + file_path: path.to_string_lossy().to_string(), + file_size: metadata.len() as i64, + description: Some(format!("Auto-imported plugin: {}", name)), + version: "1.0.0".to_string(), + author: None, + tags: vec!["auto-imported".to_string()], + }; + + match self.create_plugin(request).await { + Ok(plugin) => synced_plugins.push(plugin), + Err(e) => eprintln!("Failed to create plugin {}: {}", name, e), + } + } + } + } + + Ok(synced_plugins) + } +} \ No newline at end of file diff --git a/sandcrate-backend/src/websocket.rs b/sandcrate-backend/src/websocket.rs index 23c0ca2..e044760 100644 --- a/sandcrate-backend/src/websocket.rs +++ b/sandcrate-backend/src/websocket.rs @@ -1,15 +1,21 @@ use axum::{ - extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State}, + extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State, Query}, response::IntoResponse, }; use serde_json::json; use std::sync::Arc; use tokio::sync::broadcast; use uuid::Uuid; +use serde::Deserialize; use crate::auth::AuthConfig; use crate::plugin; +#[derive(Debug, Deserialize)] +pub struct WebSocketQuery { + token: Option, +} + #[derive(Debug, Clone)] pub struct PluginExecutionSession { pub id: String, @@ -36,10 +42,28 @@ impl WebSocketManager { pub async fn plugin_execution_websocket( ws: WebSocketUpgrade, State((state, ws_manager)): State<(Arc, Arc)>, + Query(query): Query, ) -> impl IntoResponse { + if let Some(token) = query.token { + if !is_valid_token(&token, &state).await { + return axum::http::Response::builder() + .status(401) + .body("Unauthorized".into()) + .unwrap(); + } + } + ws.on_upgrade(|socket| handle_plugin_execution_socket(socket, state, ws_manager)) } +async fn is_valid_token(token: &str, _config: &AuthConfig) -> bool { + if token.is_empty() { + return false; + } + + true +} + async fn handle_plugin_execution_socket( mut socket: WebSocket, _state: Arc, @@ -88,13 +112,13 @@ async fn handle_plugin_execution_socket( let plugin_id = plugin_id.to_string(); tokio::spawn(async move { - let result = plugin::run_plugin_with_realtime_output( - &format!("assets/plugins/{}.wasm", plugin_id), - parameters, - timeout, - ws_tx.clone(), - &session_id, - ).await; + let result = plugin::run_plugin_with_realtime_output( + &format!("../assets/plugins/{}.wasm", plugin_id), + parameters, + timeout, + ws_tx.clone(), + &session_id, + ).await; let final_message = match result { Ok(output) => json!({ diff --git a/sandcrate-cli/src/main.rs b/sandcrate-cli/src/main.rs index b42cf54..81a2b57 100644 --- a/sandcrate-cli/src/main.rs +++ b/sandcrate-cli/src/main.rs @@ -1,3 +1,3 @@ fn main() { - sandcrate_backend::run_plugin("assets/plugins/plugin_hello.wasm").unwrap(); + sandcrate_backend::run_plugin("../assets/plugins/plugin_hello.wasm").unwrap(); } diff --git a/sandcrate-plugin/src/lib.rs b/sandcrate-plugin/src/lib.rs index b09ae31..d06065f 100644 --- a/sandcrate-plugin/src/lib.rs +++ b/sandcrate-plugin/src/lib.rs @@ -3,17 +3,12 @@ use std::os::raw::c_char; #[no_mangle] pub extern "C" fn main() { - println!("Hello from WASM plugin!"); println!("Plugin execution started"); - - // Try to get parameters if available if let Some(params) = get_parameters() { println!("Received parameters: {}", params); } else { println!("No parameters provided"); } - - // Simulate some work for i in 1..=3 { println!("Processing step {}", i); } @@ -28,12 +23,8 @@ pub extern "C" fn run() { } fn get_parameters() -> Option { - // This would be called by the host environment - // For now, we'll return None to indicate no parameters None } - -// Export a function that can be called from the host #[no_mangle] pub extern "C" fn process_data(input: *const c_char) -> *const c_char { if input.is_null() { @@ -45,8 +36,5 @@ pub extern "C" fn process_data(input: *const c_char) -> *const c_char { }; let result = format!("Processed: {}", input_str); - - // In a real implementation, you'd need to manage memory properly - // For this demo, we'll just return a static string "Data processed successfully\0".as_ptr() as *const c_char } diff --git a/sandcrate-react/src/components/LoadingSpinner.tsx b/sandcrate-react/src/components/LoadingSpinner.tsx index 90dca96..e7c6c40 100644 --- a/sandcrate-react/src/components/LoadingSpinner.tsx +++ b/sandcrate-react/src/components/LoadingSpinner.tsx @@ -24,7 +24,6 @@ export const LoadingSpinner: React.FC = ({ ); }; -// Full screen loading spinner export const FullScreenSpinner: React.FC = () => { return (
diff --git a/sandcrate-react/src/components/RealtimePluginExecutor.tsx b/sandcrate-react/src/components/RealtimePluginExecutor.tsx index 25e4222..6ca32fc 100644 --- a/sandcrate-react/src/components/RealtimePluginExecutor.tsx +++ b/sandcrate-react/src/components/RealtimePluginExecutor.tsx @@ -63,7 +63,10 @@ export const RealtimePluginExecutor: React.FC = ({ const connectWebSocket = () => { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `ws://127.0.0.1:3000/ws/plugins`; + const token = localStorage.getItem('authToken'); + const wsUrl = token + ? `ws://127.0.0.1:3000/ws/plugins?token=${encodeURIComponent(token)}` + : `ws://127.0.0.1:3000/ws/plugins`; console.log('Attempting to connect to WebSocket:', wsUrl); @@ -152,13 +155,11 @@ export const RealtimePluginExecutor: React.FC = ({ const executePlugin = () => { if (!plugin || !isConnected || !wsRef.current) return; - // Clear previous output setOutput([]); setError(null); setIsExecuting(true); setExecutionStatus('starting'); - // Parse parameters let parsedParams = {}; if (parameters.trim()) { try { @@ -170,12 +171,11 @@ export const RealtimePluginExecutor: React.FC = ({ } } - // Send execution command const command = { command: 'execute_plugin', plugin_id: plugin.id, parameters: parsedParams, - timeout: 30000 // 30 seconds timeout + timeout: 30000 }; wsRef.current.send(JSON.stringify(command)); @@ -196,7 +196,6 @@ export const RealtimePluginExecutor: React.FC = ({ setExecutionStatus('idle'); }; - // Connect to WebSocket when modal opens useEffect(() => { if (isOpen && !isConnected) { console.log('Modal opened, attempting WebSocket connection...'); diff --git a/sandcrate-react/src/components/Sidebar.tsx b/sandcrate-react/src/components/Sidebar.tsx index 8edda7f..1938de5 100644 --- a/sandcrate-react/src/components/Sidebar.tsx +++ b/sandcrate-react/src/components/Sidebar.tsx @@ -21,12 +21,10 @@ export const Sidebar: React.FC = () => { return (
- {/* Logo */}

SandCrate

- {/* Navigation */} - {/* User Profile */}
@@ -59,7 +56,6 @@ export const Sidebar: React.FC = () => {
{error && ( @@ -320,7 +407,6 @@ export const Plugins: React.FC = () => {
)} - {/* Search */}
@@ -338,14 +424,12 @@ export const Plugins: React.FC = () => {
- {/* Plugins Grid */}
{(filteredPlugins || []).map((plugin) => { const lastExecution = getLastExecutionResult(plugin.id); return (
- {/* Plugin Header */}

{plugin.name}

@@ -367,7 +451,6 @@ export const Plugins: React.FC = () => {
- {/* Plugin Details */}
Filename: @@ -389,7 +472,6 @@ export const Plugins: React.FC = () => { )}
- {/* Actions */}
+
@@ -435,7 +524,6 @@ export const Plugins: React.FC = () => {
)} - {/* Execution Modal */} { onExecute={executePlugin} /> - {/* Real-time Execution Modal */} setIsRealtimeModalOpen(false)} /> + + {isUploadModalOpen && ( +
+
+
+

Upload Plugin

+ +
+
+
+
+ + { + const file = e.target.files?.[0]; + if (file) { + handleUploadPlugin(file); + } + }} + className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" + /> +
+

+ Upload a compiled WASM plugin file. The file will be saved to the assets/plugins directory. +

+
+
+
+
+ )}
); }; \ No newline at end of file diff --git a/sandcrate-react/vite.config.ts b/sandcrate-react/vite.config.ts index bd06d7c..0b34b11 100644 --- a/sandcrate-react/vite.config.ts +++ b/sandcrate-react/vite.config.ts @@ -1,7 +1,5 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' - -// https://vite.dev/config/ export default defineConfig({ plugins: [react()], server: { diff --git a/setup_postgresql.sh b/setup_postgresql.sh new file mode 100755 index 0000000..7261499 --- /dev/null +++ b/setup_postgresql.sh @@ -0,0 +1,178 @@ +#!/bin/bash + +set -e + +echo "Setting up PostgreSQL for Sandcrate..." +echo "==========================================" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_status "Checking PostgreSQL installation..." +if ! command -v psql &> /dev/null; then + print_error "PostgreSQL is not installed." + echo "" + echo "Please install PostgreSQL first:" + echo "" + echo "Ubuntu/Debian:" + echo " sudo apt-get update" + echo " sudo apt-get install postgresql postgresql-contrib" + echo " sudo systemctl start postgresql" + echo " sudo systemctl enable postgresql" + echo "" + echo "macOS:" + echo " brew install postgresql" + echo " brew services start postgresql" + echo "" + echo "Windows:" + echo " Download from https://www.postgresql.org/download/windows/" + exit 1 +fi + +print_success "PostgreSQL is installed" + +print_status "Checking PostgreSQL service..." +if ! pg_isready -q; then + print_error "PostgreSQL service is not running." + echo "" + echo "Please start PostgreSQL:" + echo " Ubuntu/Debian: sudo systemctl start postgresql" + echo " macOS: brew services start postgresql" + exit 1 +fi + +print_success "PostgreSQL service is running" + +PG_VERSION=$(psql --version | grep -oP '\d+\.\d+' | head -1) +print_status "PostgreSQL version: $PG_VERSION" + +print_status "Setting up database and user..." + +if psql -U postgres -c "SELECT 1 FROM pg_roles WHERE rolname='sandcrate'" | grep -q 1; then + print_warning "User 'sandcrate' already exists" +else + psql -U postgres -c "CREATE USER sandcrate WITH PASSWORD 'sandcrate';" + print_success "Created user 'sandcrate'" +fi + +if psql -U postgres -lqt | cut -d \| -f 1 | grep -qw sandcrate; then + print_warning "Database 'sandcrate' already exists" +else + psql -U postgres -c "CREATE DATABASE sandcrate OWNER sandcrate;" + print_success "Created database 'sandcrate'" +fi + +psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE sandcrate TO sandcrate;" +psql -U postgres -c "GRANT ALL ON SCHEMA public TO sandcrate;" + +print_success "Database setup completed" + +print_status "Checking SQLx CLI installation..." +if ! command -v sqlx &> /dev/null; then + print_warning "SQLx CLI is not installed. Installing..." + cargo install sqlx-cli --no-default-features --features postgres + print_success "SQLx CLI installed" +else + print_success "SQLx CLI is already installed" +fi + +print_status "Setting up environment configuration..." +if [ ! -f "sandcrate-backend/.env" ]; then + if [ -f "sandcrate-backend/env.example" ]; then + cp sandcrate-backend/env.example sandcrate-backend/.env + print_success "Created .env file from template" + else + print_warning "env.example not found, creating basic .env file" + cat > sandcrate-backend/.env << EOF +DATABASE_URL=postgresql://sandcrate:sandcrate@localhost:5432/sandcrate +JWT_SECRET=your-super-secret +JWT_EXPIRATION_HOURS=24 +SERVER_HOST=127.0.0.1 +SERVER_PORT=3000 +PLUGINS_DIR=../assets/plugins +MAX_PLUGIN_SIZE_MB=50 +LOG_LEVEL=info +EOF + print_success "Created basic .env file" + fi +else + print_warning ".env file already exists" +fi + +print_status "Running database migrations..." +cd sandcrate-backend + +export DATABASE_URL="postgresql://sandcrate:sandcrate@localhost:5432/sandcrate" + +if [ -d "migrations" ]; then + if sqlx migrate run; then + print_success "Database migrations completed" + else + print_error "Failed to run migrations" + exit 1 + fi +else + print_warning "No migrations directory found" +fi + +print_status "Testing database connection..." +if psql -U sandcrate -d sandcrate -c "SELECT version();" > /dev/null 2>&1; then + print_success "Database connection test passed" +else + print_error "Database connection test failed" + exit 1 +fi + +print_status "Initializing database with plugins..." +if cargo run --bin init_db 2>/dev/null || cargo run --example init_db 2>/dev/null; then + print_success "Database initialization completed" +else + print_warning "Database initialization script not found or failed" +fi + +cd .. + +echo "" +echo "==========================================" +print_success "PostgreSQL setup completed successfully!" +echo "" +echo "Configuration Summary:" +echo " Database: sandcrate" +echo " User: sandcrate" +echo " Password: sandcrate" +echo " Host: localhost" +echo " Port: 5432" +echo "" +echo "Connection URL:" +echo " postgresql://sandcrate:sandcrate@localhost:5432/sandcrate" +echo "" +echo "Environment file:" +echo " sandcrate-backend/.env" +echo "" +echo "Next steps:" +echo "1. Start the backend: cd sandcrate-backend && cargo run" +echo "2. Start the frontend: cd sandcrate-react && npm run dev" +echo "3. Access the application at http://localhost:5173" +echo "" +echo "Documentation:" +echo " See sandcrate-backend/DATABASE.md for detailed information" +echo "" \ No newline at end of file diff --git a/test_websocket.py b/test_websocket.py deleted file mode 100644 index b09b77c..0000000 --- a/test_websocket.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -import asyncio -import websockets -import json - -async def test_websocket(): - uri = "ws://localhost:3000/ws/plugins" - - try: - async with websockets.connect(uri) as websocket: - print("Connected to WebSocket") - - # Send a test message - test_message = { - "command": "execute_plugin", - "plugin_id": "plugin_hello", - "parameters": {"test": "data"}, - "timeout": 10000 - } - - await websocket.send(json.dumps(test_message)) - print(f"Sent: {test_message}") - - # Listen for responses - count = 0 - while count < 10: # Listen for up to 10 messages - try: - response = await asyncio.wait_for(websocket.recv(), timeout=5.0) - print(f"Received: {response}") - count += 1 - - # Parse the response - data = json.loads(response) - if data.get("type") == "result": - print("Plugin execution completed!") - break - - except asyncio.TimeoutError: - print("Timeout waiting for response") - break - - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - asyncio.run(test_websocket()) \ No newline at end of file diff --git a/test_websocket_simple.js b/test_websocket_simple.js deleted file mode 100644 index 4dcb379..0000000 --- a/test_websocket_simple.js +++ /dev/null @@ -1,61 +0,0 @@ -const WebSocket = require('ws'); - -async function testWebSocket() { - const ws = new WebSocket('ws://localhost:3000/ws/plugins'); - - ws.on('open', function open() { - console.log('✅ WebSocket connected successfully'); - - // Send a test message - const testMessage = { - command: 'execute_plugin', - plugin_id: 'plugin_hello', - parameters: { test: 'data' }, - timeout: 10000 - }; - - console.log('📤 Sending test message:', JSON.stringify(testMessage)); - ws.send(JSON.stringify(testMessage)); - }); - - ws.on('message', function message(data) { - try { - const parsed = JSON.parse(data.toString()); - console.log('📥 Received:', JSON.stringify(parsed, null, 2)); - - if (parsed.type === 'result') { - console.log('✅ Plugin execution completed!'); - ws.close(); - } - } catch (e) { - console.log('📥 Received raw:', data.toString()); - } - }); - - ws.on('close', function close() { - console.log('🔌 WebSocket connection closed'); - }); - - ws.on('error', function error(err) { - console.error('❌ WebSocket error:', err.message); - }); - - // Close after 10 seconds - setTimeout(() => { - console.log('⏰ Timeout reached, closing connection'); - ws.close(); - }, 10000); -} - -// Check if ws module is available -try { - require('ws'); - testWebSocket(); -} catch (e) { - console.log('❌ WebSocket module not available. Install with: npm install ws'); - console.log('📝 You can test the WebSocket manually by:'); - console.log(' 1. Opening http://localhost:5173 in your browser'); - console.log(' 2. Going to the Plugins page'); - console.log(' 3. Clicking "Real-time" on any plugin'); - console.log(' 4. Clicking "Execute" to start real-time execution'); -} \ No newline at end of file