diff --git a/client/package-lock.json b/client/package-lock.json index 8e00429..f80bab0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -901,7 +901,6 @@ "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..16a2bdb --- /dev/null +++ b/dev.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# GroupChat2 Dev Script (macOS/Linux) +# Runs both server (Rust/Axum) and client (Vite) with unified console output. +# Ctrl+C stops both processes. + +set -e + +ROOT="$(cd "$(dirname "$0")" && pwd)" + +# Colors +MAGENTA='\033[0;35m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +GRAY='\033[0;90m' +NC='\033[0m' + +# Install client deps if needed +if [ ! -d "$ROOT/client/node_modules" ]; then + echo -e "${CYAN}[dev] Installing client dependencies...${NC}" + (cd "$ROOT/client" && npm install) +fi + +echo "" +echo -e " ${MAGENTA}GroupChat2 Dev Server${NC}" +echo -e " ${YELLOW}Server: http://localhost:3001${NC}" +echo -e " ${CYAN}Client: http://localhost:3000${NC}" +echo -e " ${GRAY}Press Ctrl+C to stop both${NC}" +echo "" + +# Track child PIDs for cleanup +SERVER_PID="" +CLIENT_PID="" + +cleanup() { + echo "" + echo -e "${RED}[dev] Shutting down...${NC}" + + # Kill background jobs + [ -n "$SERVER_PID" ] && kill "$SERVER_PID" 2>/dev/null + [ -n "$CLIENT_PID" ] && kill "$CLIENT_PID" 2>/dev/null + + # Kill any lingering processes on ports 3000/3001 + lsof -ti:3000 2>/dev/null | xargs kill -9 2>/dev/null || true + lsof -ti:3001 2>/dev/null | xargs kill -9 2>/dev/null || true + + # Kill groupchat-server if still running + pkill -f "groupchat-server" 2>/dev/null || true + + echo -e "${RED}[dev] Stopped.${NC}" + exit 0 +} + +trap cleanup INT TERM + +# Start server (Rust/Axum) +(cd "$ROOT/server" && cargo run 2>&1 | while IFS= read -r line; do + [ -n "$line" ] && echo -e "${YELLOW}[server]${NC} $line" +done) & +SERVER_PID=$! + +# Start client (Vite) +(cd "$ROOT/client" && npm run dev 2>&1 | while IFS= read -r line; do + [ -n "$line" ] && echo -e "${CYAN}[client]${NC} $line" +done) & +CLIENT_PID=$! + +# Wait for either to exit +wait -n "$SERVER_PID" "$CLIENT_PID" 2>/dev/null +echo -e "${RED}[dev] A process exited unexpectedly.${NC}" +cleanup diff --git a/prod.sh b/prod.sh new file mode 100755 index 0000000..6fd642c --- /dev/null +++ b/prod.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# GroupChat2 Production Build & Run Script +# Builds the client, compiles the server in release mode, and runs it. +# The Rust server serves the static client files directly — no Vite needed. +# +# Usage: +# ./prod.sh # Build + run +# ./prod.sh build # Build only (no run) +# ./prod.sh run # Run only (skip build, assumes already built) + +set -e + +ROOT="$(cd "$(dirname "$0")" && pwd)" + +# Colors +MAGENTA='\033[0;35m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +GREEN='\033[0;32m' +GRAY='\033[0;90m' +NC='\033[0m' + +MODE="${1:-all}" # all | build | run + +# ─── Preflight checks ─────────────────────────────────────────────── + +check_env() { + if [ ! -f "$ROOT/server/.env" ]; then + echo -e "${RED}[prod] Missing server/.env file!${NC}" + echo -e "${GRAY} Copy server/.env.example and fill in your keys:${NC}" + echo -e "${GRAY} cp server/.env.example server/.env${NC}" + exit 1 + fi + + # Source .env to validate required vars + set -a + source "$ROOT/server/.env" + set +a + + if [ -z "$OPENROUTER_API_KEY" ] || [ "$OPENROUTER_API_KEY" = "your-openrouter-api-key-here" ]; then + echo -e "${RED}[prod] OPENROUTER_API_KEY not set in server/.env${NC}" + exit 1 + fi + if [ -z "$BRAVE_API_KEY" ] || [ "$BRAVE_API_KEY" = "your-brave-api-key-here" ]; then + echo -e "${RED}[prod] BRAVE_API_KEY not set in server/.env${NC}" + exit 1 + fi + if [ -z "$JWT_SECRET" ] || [ "$JWT_SECRET" = "dev-secret-change-me" ]; then + echo -e "${YELLOW}[prod] WARNING: Using default JWT_SECRET — set a real secret for production!${NC}" + fi +} + +# ─── Build ─────────────────────────────────────────────────────────── + +build() { + echo -e "${MAGENTA}[prod] Building GroupChat2 for production...${NC}" + echo "" + + # 1. Install client deps if needed + if [ ! -d "$ROOT/client/node_modules" ]; then + echo -e "${CYAN}[prod] Installing client dependencies...${NC}" + (cd "$ROOT/client" && npm install) + fi + + # 2. Build client (Vite → dist/) + echo -e "${CYAN}[prod] Building client...${NC}" + (cd "$ROOT/client" && npm run build) + echo -e "${GREEN}[prod] Client built → client/dist/${NC}" + + # 3. Build server in release mode + echo -e "${YELLOW}[prod] Building server (release)...${NC}" + (cd "$ROOT/server" && cargo build --release) + echo -e "${GREEN}[prod] Server built → server/target/release/groupchat-server${NC}" + + echo "" + echo -e "${GREEN}[prod] Build complete!${NC}" +} + +# ─── Run ───────────────────────────────────────────────────────────── + +run() { + check_env + + # Verify build artifacts exist + if [ ! -f "$ROOT/server/target/release/groupchat-server" ]; then + echo -e "${RED}[prod] Server binary not found. Run './prod.sh build' first.${NC}" + exit 1 + fi + if [ ! -d "$ROOT/client/dist" ]; then + echo -e "${RED}[prod] Client dist not found. Run './prod.sh build' first.${NC}" + exit 1 + fi + + BIND_ADDR="${BIND_ADDR:-0.0.0.0:3001}" + + echo "" + echo -e " ${MAGENTA}GroupChat2 Production${NC}" + echo -e " ${GREEN}Domain: https://gchat.tud.ink${NC}" + echo -e " ${YELLOW}Bind: ${BIND_ADDR}${NC}" + echo -e " ${GRAY}Press Ctrl+C to stop${NC}" + echo "" + + # Source .env so the server binary picks up the vars + set -a + source "$ROOT/server/.env" + set +a + + export STATIC_DIR="$ROOT/client/dist" + export BIND_ADDR + export RUST_LOG="${RUST_LOG:-info}" + + cleanup() { + echo "" + echo -e "${RED}[prod] Shutting down...${NC}" + kill "$SERVER_PID" 2>/dev/null || true + lsof -ti:3001 2>/dev/null | xargs kill -9 2>/dev/null || true + echo -e "${RED}[prod] Stopped.${NC}" + exit 0 + } + trap cleanup INT TERM + + # Run the release binary + "$ROOT/server/target/release/groupchat-server" & + SERVER_PID=$! + + wait "$SERVER_PID" +} + +# ─── Main ──────────────────────────────────────────────────────────── + +case "$MODE" in + build) + build + ;; + run) + run + ;; + all|"") + build + run + ;; + *) + echo "Usage: $0 [build|run]" + echo " (no args) Build and run" + echo " build Build only" + echo " run Run only (must build first)" + exit 1 + ;; +esac diff --git a/server/.env.example b/server/.env.example index 34f615f..7da919b 100644 --- a/server/.env.example +++ b/server/.env.example @@ -10,3 +10,9 @@ JWT_SECRET=change-me-to-a-random-secret # OpenRouter API OPENROUTER_API_KEY=sk-or-v1-your-key-here + +# Brave Search API +BRAVE_API_KEY=your-brave-api-key-here + +# Production: path to built client files (default: ../client/dist) +# STATIC_DIR=../client/dist diff --git a/server/src/main.rs b/server/src/main.rs index d9ac180..153bf8c 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,6 +11,7 @@ use sqlx::sqlite::SqlitePoolOptions; use std::sync::Arc; use tokio::sync::broadcast; use tower_http::cors::{Any, CorsLayer}; +use tower_http::services::{ServeDir, ServeFile}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; pub struct AppState { @@ -97,7 +98,10 @@ async fn main() { .allow_methods(Any) .allow_headers(Any); - let app = Router::new() + // Serve static files from client dist in production + let static_dir = std::env::var("STATIC_DIR").unwrap_or_else(|_| "../client/dist".into()); + + let api_routes = Router::new() // Auth routes .route("/api/auth/register", post(handlers::auth::register)) .route("/api/auth/login", post(handlers::auth::login)) @@ -118,6 +122,12 @@ async fn main() { .layer(cors) .with_state(state); + // SPA fallback: serve static assets, fall back to index.html for client-side routing + let spa = ServeDir::new(&static_dir) + .not_found_service(ServeFile::new(format!("{}/index.html", static_dir))); + + let app = api_routes.fallback_service(spa); + let addr = std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0:3001".into()); tracing::info!("Server starting on {}", addr); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();