diff --git a/Cargo.toml b/Cargo.toml index 887ddb6..f60f02f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "2" members = [ "sandcrate-backend", - "sandcrate-web", "sandcrate-cli", "sandcrate-plugin" ] diff --git a/sandcrate-backend/src/api.rs b/sandcrate-backend/src/api.rs index ba0bbb2..6592dd7 100644 --- a/sandcrate-backend/src/api.rs +++ b/sandcrate-backend/src/api.rs @@ -1,18 +1,74 @@ -use axum::{routing::get, Json, Router}; +use axum::{routing::get, Json, Router, extract::State}; use serde::Serialize; +use std::sync::Arc; +use std::fs; +use std::path::Path; -use crate::plugin::list_plugins; +use crate::auth::AuthConfig; + +#[derive(Serialize)] +struct Plugin { + id: String, + name: String, + filename: String, + size: u64, + created_at: String, +} #[derive(Serialize)] struct PluginList { - plugins: Vec, + plugins: Vec, } -async fn get_plugins() -> Json { - let plugins = list_plugins(); +async fn get_plugins( + State(_config): State>, +) -> Json { + let plugins_dir = Path::new("../assets/plugins"); + let mut plugins = Vec::new(); + + if let Ok(entries) = fs::read_dir(plugins_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(extension) = path.extension() { + if extension == "wasm" { + if let Ok(metadata) = fs::metadata(&path) { + let filename = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown") + .to_string(); + + let name = filename.replace(".wasm", ""); + let id = name.clone(); + + let created_at = metadata.created() + .ok() + .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|duration| { + chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0) + .unwrap_or_default() + .format("%Y-%m-%d %H:%M:%S") + .to_string() + }) + .unwrap_or_else(|| "Unknown".to_string()); + + plugins.push(Plugin { + id, + name, + filename, + size: metadata.len(), + created_at, + }); + } + } + } + } + } + } + Json(PluginList { plugins }) } -pub fn routes() -> Router { +pub fn routes() -> Router> { Router::new().route("/plugins", get(get_plugins)) } diff --git a/sandcrate-backend/src/auth.rs b/sandcrate-backend/src/auth.rs index 2048af8..de373e9 100644 --- a/sandcrate-backend/src/auth.rs +++ b/sandcrate-backend/src/auth.rs @@ -2,11 +2,15 @@ use axum::{ extract::State, http::StatusCode, response::Json, - routing::post, + routing::{get, post}, Router, }; +use axum_extra::{ + extract::TypedHeader, + headers::{authorization::Bearer, Authorization}, +}; use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use pam::Authenticator; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -27,10 +31,19 @@ pub struct LoginRequest { #[derive(Debug, Serialize)] pub struct LoginResponse { pub token: String, - pub username: String, + pub user: UserInfo, pub expires_at: String, } +#[derive(Debug, Serialize)] +pub struct UserInfo { + pub id: String, + pub username: String, + pub name: String, + pub role: String, + pub is_admin: bool, +} + #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: String, @@ -48,17 +61,45 @@ impl AuthConfig { } } +// Check if user has sudo/root privileges +fn check_user_privileges(username: &str) -> (bool, String) { + // Check if user is root + if username == "root" { + return (true, "root".to_string()); + } + + // Check if user has sudo privileges + let output = std::process::Command::new("sudo") + .args(["-l", "-U", username]) + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout); + if output_str.contains("(ALL : ALL)") || output_str.contains("(root)") { + return (true, "sudo".to_string()); + } + } + } + Err(_) => {} + } + + (false, "user".to_string()) +} + pub async fn login( State(config): State>, 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(|_| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { - error: "Failed to initialize PAM".to_string(), + error: "Failed to initialize PAM authentication".to_string(), }), ) })?; @@ -69,6 +110,24 @@ 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() + .ok() + .and_then(|output| { + if output.status.success() { + let line = String::from_utf8_lossy(&output.stdout); + line.split(':').nth(4).map(|s| s.trim().to_string()) + } else { + None + } + }) + .unwrap_or_else(|| payload.username.clone()); + // Generate JWT token let now = Utc::now(); let expires_at = now + Duration::hours(24); @@ -88,26 +147,91 @@ pub async fn login( ( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { - error: "Failed to generate token".to_string(), + error: "Failed to generate authentication token".to_string(), }), ) })?; + let user_info = UserInfo { + id: payload.username.clone(), + username: payload.username.clone(), + name: real_name, + role, + is_admin, + }; + + println!("Login successful for user: {}", payload.username); Ok(Json(LoginResponse { token, - username: payload.username, + user: user_info, expires_at: expires_at.to_rfc3339(), })) } Err(_) => Err(( StatusCode::UNAUTHORIZED, Json(ErrorResponse { - error: "Invalid credentials".to_string(), + error: "Invalid username or password".to_string(), }), )), } } +pub async fn validate_token( + State(config): State>, + TypedHeader(Authorization(bearer)): TypedHeader>, +) -> Result, (StatusCode, Json)> { + let token = bearer.token(); + + println!("Validating token: {}", token); + + let token_data = decode::( + token, + &DecodingKey::from_secret(config.jwt_secret.as_ref()), + &Validation::default(), + ) + .map_err(|e| { + println!("Token validation error: {:?}", e); + ( + StatusCode::UNAUTHORIZED, + Json(ErrorResponse { + error: "Invalid or expired token".to_string(), + }), + ) + })?; + + let username = token_data.claims.sub; + println!("Token validated for user: {}", username); + + let (is_admin, role) = check_user_privileges(&username); + + let real_name = std::process::Command::new("getent") + .args(["passwd", &username]) + .output() + .ok() + .and_then(|output| { + if output.status.success() { + let line = String::from_utf8_lossy(&output.stdout); + line.split(':').nth(4).map(|s| s.trim().to_string()) + } else { + None + } + }) + .unwrap_or_else(|| username.clone()); + + let user_info = UserInfo { + id: username.clone(), + username, + name: real_name, + role, + is_admin, + }; + + println!("Returning user info: {:?}", user_info); + Ok(Json(user_info)) +} + pub fn auth_routes() -> Router> { - Router::new().route("/login", post(login)) + Router::new() + .route("/login", post(login)) + .route("/validate", get(validate_token)) } \ No newline at end of file diff --git a/sandcrate-backend/src/main.rs:3:38 b/sandcrate-backend/src/main.rs:3:38 deleted file mode 100644 index e69de29..0000000 diff --git a/sandcrate-react/package-lock.json b/sandcrate-react/package-lock.json index 76acc5e..deee065 100644 --- a/sandcrate-react/package-lock.json +++ b/sandcrate-react/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@headlessui/react": "^2.2.6", + "@heroicons/react": "^2.2.0", + "@tailwindcss/postcss": "^4.1.11", "lucide-react": "^0.526.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -32,11 +34,22 @@ "vite": "^7.0.4" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -997,6 +1010,15 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1063,11 +1085,22 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1078,7 +1111,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1088,14 +1120,12 @@ "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.29", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1533,6 +1563,267 @@ "tslib": "^2.8.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", + "integrity": "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "postcss": "^8.4.41", + "tailwindcss": "4.1.11" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.13.12", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", @@ -2154,6 +2445,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2253,6 +2553,15 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.191", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", @@ -2260,6 +2569,19 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.8", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", @@ -2693,6 +3015,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2787,6 +3115,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2878,6 +3215,234 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2920,6 +3485,15 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2957,6 +3531,42 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2968,7 +3578,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -3094,7 +3703,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3114,7 +3722,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3390,7 +3997,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -3432,9 +4038,43 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "dev": true, "license": "MIT" }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", diff --git a/sandcrate-react/package.json b/sandcrate-react/package.json index 7439293..b960485 100644 --- a/sandcrate-react/package.json +++ b/sandcrate-react/package.json @@ -11,6 +11,8 @@ }, "dependencies": { "@headlessui/react": "^2.2.6", + "@heroicons/react": "^2.2.0", + "@tailwindcss/postcss": "^4.1.11", "lucide-react": "^0.526.0", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/sandcrate-react/postcss.config.js b/sandcrate-react/postcss.config.js new file mode 100644 index 0000000..5132039 --- /dev/null +++ b/sandcrate-react/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/sandcrate-react/src/App.tsx b/sandcrate-react/src/App.tsx index 3d7ded3..d521a84 100644 --- a/sandcrate-react/src/App.tsx +++ b/sandcrate-react/src/App.tsx @@ -1,35 +1,68 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { LoadingSpinner } from './components/LoadingSpinner'; +import { Sidebar } from './components/Sidebar'; +import { Auth } from './pages/Auth'; +import { Dashboard } from './pages/Dashboard'; +import { Plugins } from './pages/Plugins'; +import './index.css'; -function App() { - const [count, setCount] = useState(0) +// Protected Route Component +const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { user, loading } = useAuth(); + + if (loading) { + return ; + } + + if (!user) { + return ; + } + + return <>{children}; +}; +// Main Layout Component +const MainLayout: React.FC = () => { + return ( +
+ +
+ + } /> + } /> + } /> + +
+
+ ); +}; + +// Main App Component +function App() { return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) + + + +
+ + } /> + + + + } + /> + +
+
+
+
+ ); } -export default App +export default App; diff --git a/sandcrate-react/src/components/ErrorBoundary.tsx b/sandcrate-react/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..e52ca49 --- /dev/null +++ b/sandcrate-react/src/components/ErrorBoundary.tsx @@ -0,0 +1,78 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.hasError) { + return ( +
+
+
+ + + +
+
+

+ Something went wrong +

+

+ We're sorry, but something unexpected happened. Please try refreshing the page. +

+
+ +
+ {process.env.NODE_ENV === 'development' && this.state.error && ( +
+ + Error Details (Development) + +
+                    {this.state.error.toString()}
+                  
+
+ )} +
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/sandcrate-react/src/components/LoadingSpinner.tsx b/sandcrate-react/src/components/LoadingSpinner.tsx new file mode 100644 index 0000000..90dca96 --- /dev/null +++ b/sandcrate-react/src/components/LoadingSpinner.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +interface LoadingSpinnerProps { + size?: 'sm' | 'md' | 'lg'; + className?: string; +} + +export const LoadingSpinner: React.FC = ({ + size = 'md', + className = '' +}) => { + const sizeClasses = { + sm: 'w-4 h-4', + md: 'w-8 h-8', + lg: 'w-12 h-12' + }; + + return ( +
+
+
+ ); +}; + +// Full screen loading spinner +export const FullScreenSpinner: React.FC = () => { + return ( +
+
+ +

Loading...

+
+
+ ); +}; \ No newline at end of file diff --git a/sandcrate-react/src/components/Sidebar.tsx b/sandcrate-react/src/components/Sidebar.tsx new file mode 100644 index 0000000..d9a0aab --- /dev/null +++ b/sandcrate-react/src/components/Sidebar.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { useAuth } from '../contexts/AuthContext'; +import { + HomeIcon, + PuzzlePieceIcon, + UserCircleIcon, + ArrowRightOnRectangleIcon +} from '@heroicons/react/24/outline'; + +export const Sidebar: React.FC = () => { + const { user, logout } = useAuth(); + const location = useLocation(); + + const navigation = [ + { name: 'Dashboard', href: '/dashboard', icon: HomeIcon }, + { name: 'Plugins', href: '/plugins', icon: PuzzlePieceIcon }, + ]; + + const isActive = (path: string) => location.pathname === path; + + return ( +
+ {/* Logo */} +
+

SandCrate

+
+ + {/* Navigation */} + + + {/* User Profile */} +
+
+ +
+

{user?.name}

+

{user?.email}

+
+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/sandcrate-react/src/contexts/AuthContext.tsx b/sandcrate-react/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..732f41a --- /dev/null +++ b/sandcrate-react/src/contexts/AuthContext.tsx @@ -0,0 +1,132 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import type { ReactNode } from 'react'; + +interface User { + id: string; + username: string; + name: string; + role: string; + is_admin: boolean; +} + +interface AuthContextType { + user: User | null; + loading: boolean; + login: (username: string, password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(undefined); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check for existing session on app load + const checkAuth = async () => { + try { + const token = localStorage.getItem('authToken'); + console.log('Checking auth, token exists:', !!token); + + if (token) { + // Validate token with Rust backend + const response = await fetch('/auth/validate', { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + console.log('Validation response status:', response.status); + + if (response.ok) { + const data = await response.json(); + console.log('User data received:', data); + setUser(data); + } else { + console.log('Token validation failed, clearing storage'); + // Token is invalid, clear storage + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + } + } + } catch (error) { + console.error('Auth check failed:', error); + // Clear invalid tokens on error + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + } finally { + setLoading(false); + } + }; + + checkAuth(); + }, []); + + const login = async (username: string, password: string) => { + setLoading(true); + try { + // Call Rust backend API for PAM Linux authentication + const response = await fetch('/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: username, + password: password, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || 'Authentication failed'); + } + + const data = await response.json(); + + // Store authentication token and user data + localStorage.setItem('authToken', data.token); + localStorage.setItem('user', JSON.stringify(data.user)); + setUser(data.user); + + // Login successful - user will be redirected to dashboard + } catch (error) { + console.error('Login failed:', error); + throw new Error(error instanceof Error ? error.message : 'Login failed. Please check your credentials.'); + } finally { + setLoading(false); + } + }; + + const logout = () => { + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + setUser(null); + }; + + const value: AuthContextType = { + user, + loading, + login, + logout + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/sandcrate-react/src/index.css b/sandcrate-react/src/index.css index 08a3ac9..940ce4b 100644 --- a/sandcrate-react/src/index.css +++ b/sandcrate-react/src/index.css @@ -1,68 +1,87 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); +@import "tailwindcss"; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} +@layer base { + :root { + /* Green Colors */ + --green-50: #f0fdf4; + --green-100: #dcfce7; + --green-200: #bbf7d0; + --green-300: #86efac; + --green-400: #4ade80; + --green-500: #22c55e; + --green-600: #16a34a; + --green-700: #15803d; + --green-800: #166534; + --green-900: #14532d; -h1 { - font-size: 3.2em; - line-height: 1.1; -} + /* Gray Colors */ + --gray-50: #f9fafb; + --gray-100: #f3f4f6; + --gray-200: #e5e7eb; + --gray-300: #d1d5db; + --gray-400: #9ca3af; + --gray-500: #6b7280; + --gray-600: #4b5563; + --gray-700: #374151; + --gray-800: #1f2937; + --gray-900: #111827; + } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; + html { + font-family: 'Inter', sans-serif; + } + + body { + background-color: var(--gray-50); + color: var(--gray-900); + } } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; +@layer components { + .btn-primary { + background-color: var(--green-600); + color: white; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + transition: all 0.2s; + } + + .btn-primary:hover { + background-color: var(--green-700); + } + + .btn-secondary { + background-color: white; + color: var(--gray-700); + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid var(--gray-200); + transition: all 0.2s; + } + + .btn-secondary:hover { + background-color: var(--gray-50); + border-color: var(--gray-300); + } + + .card { + background-color: white; + border: 1px solid var(--gray-200); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); } - a:hover { - color: #747bff; + + .sidebar { + background-color: white; + border-right: 1px solid var(--gray-200); } - button { - background-color: #f9f9f9; + + .header { + background-color: white; + border-bottom: 1px solid var(--gray-200); } } diff --git a/sandcrate-react/src/pages/Auth.tsx b/sandcrate-react/src/pages/Auth.tsx new file mode 100644 index 0000000..35be1bc --- /dev/null +++ b/sandcrate-react/src/pages/Auth.tsx @@ -0,0 +1,144 @@ +import React, { useState, useEffect } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { LoadingSpinner } from '../components/LoadingSpinner'; +import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; +import { Navigate } from 'react-router-dom'; + +export const Auth: React.FC = () => { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const { login, user, loading } = useAuth(); + + // Debug logging + console.log('Auth component - loading:', loading, 'user:', user); + + // Show loading spinner while checking authentication + if (loading) { + return ( +
+ +
+ ); + } + + // If user is already logged in, redirect to dashboard + if (user) { + console.log('User is logged in, redirecting to dashboard'); + return ; + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + try { + await login(username, password); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+

SandCrate

+

Secure Plugin Management Platform

+
+ + {/* Auth Form */} +
+
+

+ Sign in to SandCrate +

+

+ Use your system credentials to access SandCrate +

+
+ +
+
+ + setUsername(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" + placeholder="Enter your username" + /> +
+ +
+ +
+ setPassword(e.target.value)} + className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" + placeholder="Enter your password" + /> + +
+
+ + {error && ( +
+

{error}

+
+ )} + + +
+
+ + {/* Footer */} +
+

Secure authentication powered by PAM Linux

+
+
+
+ ); +}; \ No newline at end of file diff --git a/sandcrate-react/src/pages/Dashboard.tsx b/sandcrate-react/src/pages/Dashboard.tsx new file mode 100644 index 0000000..0582acd --- /dev/null +++ b/sandcrate-react/src/pages/Dashboard.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export const Dashboard: React.FC = () => { + return ( +
+
+

Welcome to SandCrate

+

Secure Plugin Management Platform

+ +
+
+

Plugin Management

+

Manage and monitor your WASM plugins

+
+ +
+

System Security

+

PAM Linux authentication for secure access

+
+ +
+

Real-time Monitoring

+

Track plugin performance and system status

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/sandcrate-react/src/pages/Plugins.tsx b/sandcrate-react/src/pages/Plugins.tsx new file mode 100644 index 0000000..1e34306 --- /dev/null +++ b/sandcrate-react/src/pages/Plugins.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect } from 'react'; +import { LoadingSpinner } from '../components/LoadingSpinner'; +import { + MagnifyingGlassIcon, + PuzzlePieceIcon +} from '@heroicons/react/24/outline'; + +interface Plugin { + id: string; + name: string; + filename: string; + size: number; + created_at: string; +} + +export const Plugins: React.FC = () => { + const [plugins, setPlugins] = useState([]); + const [filteredPlugins, setFilteredPlugins] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + fetchPlugins(); + }, []); + + useEffect(() => { + filterPlugins(); + }, [plugins, searchTerm]); + + const fetchPlugins = async () => { + try { + setLoading(true); + + // Fetch plugins from Rust backend + const response = await fetch('/api/plugins'); + + if (!response.ok) { + throw new Error('Failed to fetch plugins'); + } + + const data = await response.json(); + setPlugins(data.plugins); + + } catch (err) { + console.error('Plugins fetch error:', err); + setError('Failed to load plugins'); + } finally { + setLoading(false); + } + }; + + const filterPlugins = () => { + let filtered = plugins; + + // Search filter + if (searchTerm) { + filtered = filtered.filter(plugin => + plugin.name.toLowerCase().includes(searchTerm.toLowerCase()) || + plugin.filename.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + setFilteredPlugins(filtered); + }; + + const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Plugins

+

WASM plugins from assets/plugins directory

+
+
+ + {error && ( +
+

{error}

+
+ )} + + {/* Search */} +
+
+
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500" + /> +
+
+
+
+ + {/* Plugins Grid */} +
+ {filteredPlugins.map((plugin) => ( +
+ {/* Plugin Header */} +
+
+

{plugin.name}

+

WASM Plugin

+
+ + Active + +
+
+
+ + {/* Plugin Details */} +
+
+ Filename: + {plugin.filename} +
+
+ Size: + {formatFileSize(plugin.size)} +
+
+ Created: + {plugin.created_at} +
+
+ + {/* Actions */} +
+
+ +
+
+
+ ))} +
+ + {filteredPlugins.length === 0 && ( +
+ +

No plugins found

+

+ {searchTerm + ? 'Try adjusting your search terms' + : 'No WASM plugins found in assets/plugins directory' + } +

+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/sandcrate-react/vite.config.ts b/sandcrate-react/vite.config.ts index 8b0f57b..bd06d7c 100644 --- a/sandcrate-react/vite.config.ts +++ b/sandcrate-react/vite.config.ts @@ -4,4 +4,18 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false, + }, + '/auth': { + target: 'http://localhost:3000', + changeOrigin: true, + secure: false, + }, + }, + }, }) diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index dfb5c95..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import { useState } from 'react'; -import Sidebar from './components/Sidebar'; -import Dashboard from './pages/Dashboard'; -import Auth from './pages/Auth'; -import Plugins from './pages/Plugins'; -import Settings from './pages/Settings'; -import { Terminal, Server, Database, Shield, Settings as SettingsIcon } from 'lucide-react'; - -function App() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - if (!isAuthenticated) { - return setIsAuthenticated(true)} />; - } - - return ( - -
- -
- - } /> - } /> - } /> - -
-
-
- ); -} - -export default App; \ No newline at end of file diff --git a/src/app.rs b/src/app.rs deleted file mode 100644 index b1940f3..0000000 --- a/src/app.rs +++ /dev/null @@ -1,81 +0,0 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; -use serde::{Deserialize, Serialize}; -use wasm_bindgen::prelude::*; - -static CSS: Asset = asset!("/assets/styles.css"); -static TAURI_ICON: Asset = asset!("/assets/tauri.svg"); -static DIOXUS_ICON: Asset = asset!("/assets/dioxus.png"); - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] - async fn invoke(cmd: &str, args: JsValue) -> JsValue; -} - -#[derive(Serialize, Deserialize)] -struct GreetArgs<'a> { - name: &'a str, -} - -pub fn App() -> Element { - let mut name = use_signal(|| String::new()); - let mut greet_msg = use_signal(|| String::new()); - - let greet = move |_: FormEvent| async move { - if name.read().is_empty() { - return; - } - - let name = name.read(); - let args = serde_wasm_bindgen::to_value(&GreetArgs { name: &*name }).unwrap(); - // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ - let new_msg = invoke("greet", args).await.as_string().unwrap(); - greet_msg.set(new_msg); - }; - - rsx! { - link { rel: "stylesheet", href: CSS } - main { - class: "container", - h1 { "Welcome to Tauri + Dioxus" } - - div { - class: "row", - a { - href: "https://tauri.app", - target: "_blank", - img { - src: TAURI_ICON, - class: "logo tauri", - alt: "Tauri logo" - } - } - a { - href: "https://dioxuslabs.com/", - target: "_blank", - img { - src: DIOXUS_ICON, - class: "logo dioxus", - alt: "Dioxus logo" - } - } - } - p { "Click on the Tauri and Dioxus logos to learn more." } - - form { - class: "row", - onsubmit: greet, - input { - id: "greet-input", - placeholder: "Enter a name...", - value: "{name}", - oninput: move |event| name.set(event.value()) - } - button { r#type: "submit", "Greet" } - } - p { "{greet_msg}" } - } - } -} \ No newline at end of file diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx deleted file mode 100644 index abfb593..0000000 --- a/src/components/Sidebar.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { - Terminal, - Server, - Database, - Shield, - Settings, - LogOut, - Menu, - X, - Activity, - Zap, - GitBranch, - Docker -} from 'lucide-react'; - -const Sidebar = () => { - const [isCollapsed, setIsCollapsed] = useState(false); - const location = useLocation(); - - const navigation = [ - { name: 'Dashboard', href: '/', icon: Terminal }, - { name: 'Infrastructure', href: '/infrastructure', icon: Server }, - { name: 'Databases', href: '/databases', icon: Database }, - { name: 'Security', href: '/security', icon: Shield }, - { name: 'Plugins', href: '/plugins', icon: Zap }, - { name: 'Settings', href: '/settings', icon: Settings }, - ]; - - const isActive = (path: string) => location.pathname === path; - - return ( -
-
- {!isCollapsed && ( -
- - SandCrate -
- )} - -
- - -
- ); -}; - -export default Sidebar; \ No newline at end of file diff --git a/src/index.css b/src/index.css deleted file mode 100644 index af41767..0000000 --- a/src/index.css +++ /dev/null @@ -1,44 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap'); -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - html { - font-family: 'JetBrains Mono', monospace; - } - - body { - @apply bg-terminal-900 text-terminal-100; - } -} - -@layer components { - .btn-primary { - @apply bg-devops-green-600 hover:bg-devops-green-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-200; - } - - .btn-secondary { - @apply bg-terminal-700 hover:bg-terminal-600 text-terminal-100 font-medium py-2 px-4 rounded-lg transition-colors duration-200; - } - - .card { - @apply bg-terminal-800 border border-terminal-700 rounded-lg p-6 shadow-lg; - } - - .terminal-text { - @apply font-mono text-devops-green-400; - } - - .status-online { - @apply text-devops-green-400; - } - - .status-offline { - @apply text-red-400; - } - - .status-warning { - @apply text-yellow-400; - } -} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index d646cf6..0000000 --- a/src/main.rs +++ /dev/null @@ -1,10 +0,0 @@ -mod app; - -use app::App; -use dioxus::prelude::*; -use dioxus_logger::tracing::Level; - -fn main() { - dioxus_logger::init(Level::INFO).expect("failed to init logger"); - launch(App); -} diff --git a/src/pages/Auth.tsx b/src/pages/Auth.tsx deleted file mode 100644 index 7e863e1..0000000 --- a/src/pages/Auth.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState } from 'react'; -import { Terminal, Shield, Eye, EyeOff, ArrowRight } from 'lucide-react'; - -interface AuthProps { - onLogin: () => void; -} - -const Auth = ({ onLogin }: AuthProps) => { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - // Simulate API call - setTimeout(() => { - if (username === 'admin' && password === 'password') { - onLogin(); - } - setIsLoading(false); - }, 1000); - }; - - return ( -
-
- {/* Terminal Header */} -
-
-
-
-
-
-
-
- SandCrate Terminal - Authentication -
-
-
- - {/* Login Form */} -
-
-
- - -
-

SandCrate

-

DevOps Management Platform

-
- -
-
- -
- setUsername(e.target.value)} - className="w-full bg-terminal-700 border border-terminal-600 rounded-lg px-4 py-3 text-terminal-100 placeholder-terminal-400 focus:outline-none focus:ring-2 focus:ring-devops-green-500 focus:border-transparent" - placeholder="Enter username" - required - /> -
-
- -
- -
- setPassword(e.target.value)} - className="w-full bg-terminal-700 border border-terminal-600 rounded-lg px-4 py-3 pr-12 text-terminal-100 placeholder-terminal-400 focus:outline-none focus:ring-2 focus:ring-devops-green-500 focus:border-transparent" - placeholder="Enter password" - required - /> - -
-
- - -
- - {/* Demo Credentials */} -
-

Demo Credentials

-
-
- Username: - admin -
-
- Password: - password -
-
-
- - {/* Terminal Footer */} -
-
-
- System Ready -
-
-
-
-
- ); -}; - -export default Auth; \ No newline at end of file