From bc2e9c49d73f860f2e30c8b086ee3d5d738b898c Mon Sep 17 00:00:00 2001 From: mahesh bhatiya Date: Tue, 29 Jul 2025 00:00:46 +0530 Subject: [PATCH] feat: Add automatic redirect from auth page to dashboard for logged-in users - Enhance Auth component to check user authentication state on load - Add loading spinner while validating existing authentication tokens - Implement automatic redirect from /auth to /dashboard for authenticated users - Add debug logging to track authentication flow and token validation - Fix user data parsing in AuthContext (backend returns user directly, not wrapped) - Improve error handling and token cleanup for invalid sessions - Add backend logging for login attempts and token validation debugging This ensures users who are already logged in are automatically redirected to the dashboard when accessing the auth page, improving UX flow. --- Cargo.toml | 1 - sandcrate-backend/src/api.rs | 68 +- sandcrate-backend/src/auth.rs | 140 +++- sandcrate-backend/src/main.rs:3:38 | 0 sandcrate-react/package-lock.json | 660 +++++++++++++++++- sandcrate-react/package.json | 2 + sandcrate-react/postcss.config.js | 6 + sandcrate-react/src/App.tsx | 93 ++- .../src/components/ErrorBoundary.tsx | 78 +++ .../src/components/LoadingSpinner.tsx | 37 + sandcrate-react/src/components/Sidebar.tsx | 73 ++ sandcrate-react/src/contexts/AuthContext.tsx | 132 ++++ sandcrate-react/src/index.css | 137 ++-- sandcrate-react/src/pages/Auth.tsx | 144 ++++ sandcrate-react/src/pages/Dashboard.tsx | 29 + sandcrate-react/src/pages/Plugins.tsx | 180 +++++ sandcrate-react/vite.config.ts | 14 + src/App.tsx | 33 - src/app.rs | 81 --- src/components/Sidebar.tsx | 132 ---- src/index.css | 44 -- src/main.rs | 10 - src/pages/Auth.tsx | 142 ---- 23 files changed, 1680 insertions(+), 556 deletions(-) delete mode 100644 sandcrate-backend/src/main.rs:3:38 create mode 100644 sandcrate-react/postcss.config.js create mode 100644 sandcrate-react/src/components/ErrorBoundary.tsx create mode 100644 sandcrate-react/src/components/LoadingSpinner.tsx create mode 100644 sandcrate-react/src/components/Sidebar.tsx create mode 100644 sandcrate-react/src/contexts/AuthContext.tsx create mode 100644 sandcrate-react/src/pages/Auth.tsx create mode 100644 sandcrate-react/src/pages/Dashboard.tsx create mode 100644 sandcrate-react/src/pages/Plugins.tsx delete mode 100644 src/App.tsx delete mode 100644 src/app.rs delete mode 100644 src/components/Sidebar.tsx delete mode 100644 src/index.css delete mode 100644 src/main.rs delete mode 100644 src/pages/Auth.tsx 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