From 153ae3dec54dc3e8a7ce3046221d0249c8b94be5 Mon Sep 17 00:00:00 2001 From: Charmunks Date: Wed, 5 Nov 2025 14:07:53 -0500 Subject: [PATCH] spaces stuff --- .gitignore | 3 +- Dockerfile | 5 +- client/package-lock.json | 6 +- client/src/config.js | 2 +- client/src/lib/Dashboard.svelte | 41 ++++++++++++ code-server-setup.sh | 93 ++++++++++++++++++++++++++ nginx.conf | 48 +++++++++++--- src/api/spaces/space.route.js | 16 ++++- src/utils/spaces.js | 111 ++++++++++++++++++++++++++++++-- start.sh | 7 ++ 10 files changed, 312 insertions(+), 20 deletions(-) create mode 100644 code-server-setup.sh diff --git a/.gitignore b/.gitignore index a6845f5..940bb14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ node_modules dist build +AGENTS.md .github .vscode .DS_Store -.claude \ No newline at end of file +.claude diff --git a/Dockerfile b/Dockerfile index 0336493..0e45c3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y \ software-properties-common \ supervisor \ nginx \ + iptables \ && rm -rf /var/lib/apt/lists/* # Install Docker Engine for Docker-in-Docker @@ -54,7 +55,7 @@ nodaemon=true user=root [program:dockerd] -command=dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --storage-driver=overlay2 +command=dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --storage-driver=overlay2 --iptables=true --ip-forward=true --ip-masq=true autostart=true autorestart=true stderr_logfile=/var/log/supervisor/dockerd.err.log @@ -91,7 +92,7 @@ EOF # Copy and setup startup script COPY start.sh /app/start.sh -RUN chmod +x /app/start.sh +RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh # Expose ports EXPOSE 80 3000 5173 diff --git a/client/package-lock.json b/client/package-lock.json index 9c61028..f130dda 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -637,6 +637,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz", "integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==", "dev": true, + "peer": true, "engines": { "node": ">= 8" } @@ -658,6 +659,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz", "integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.15.9", "postcss": "^8.4.18", @@ -1039,7 +1041,8 @@ "version": "3.53.1", "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.53.1.tgz", "integrity": "sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==", - "dev": true + "dev": true, + "peer": true }, "svelte-hmr": { "version": "0.15.0", @@ -1053,6 +1056,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.3.tgz", "integrity": "sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.15.9", "fsevents": "~2.3.2", diff --git a/client/src/config.js b/client/src/config.js index 6157f31..bfce848 100644 --- a/client/src/config.js +++ b/client/src/config.js @@ -1,4 +1,4 @@ -export const API_BASE = 'https://t0080w08wcockgs44ws8w880.b.selfhosted.hackclub.com/api/v1'; +export const API_BASE = 'http://localhost:2593/api/v1'; export const ERROR_MESSAGES = { NETWORK_ERROR: 'Network error. Please try again.', diff --git a/client/src/lib/Dashboard.svelte b/client/src/lib/Dashboard.svelte index e62908e..319103b 100644 --- a/client/src/lib/Dashboard.svelte +++ b/client/src/lib/Dashboard.svelte @@ -173,6 +173,41 @@ } } + async function deleteSpace(spaceId) { + if (!confirm('Are you sure you want to delete this space? This action cannot be undone.')) { + return; + } + + actionLoading[spaceId] = 'deleting'; + actionError[spaceId] = ''; + actionLoading = actionLoading; + actionError = actionError; + + try { + const response = await fetch(`${API_BASE}/spaces/delete/${spaceId}`, { + method: 'DELETE', + headers: { + 'Authorization': authorization, + }, + }); + + const data = await response.json(); + + if (response.ok) { + await loadSpaces(); + } else { + actionError[spaceId] = data.error || 'Failed to delete space'; + actionError = actionError; + } + } catch (err) { + actionError[spaceId] = ERROR_MESSAGES.NETWORK_ERROR; + actionError = actionError; + } finally { + delete actionLoading[spaceId]; + actionLoading = actionLoading; + } + } + function handleSignOut() { dispatch('signout'); } @@ -389,6 +424,12 @@ > โ†ป + {/if} diff --git a/code-server-setup.sh b/code-server-setup.sh new file mode 100644 index 0000000..2411f22 --- /dev/null +++ b/code-server-setup.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# runs inside of created code-server containers, installs essential dev tools by default + +set -e + +echo "Starting development environment setup..." + +echo "Updating package manager..." +sudo apt update && sudo apt upgrade -y + +echo "Installing essential system tools..." +sudo apt install -y \ + curl \ + wget \ + git \ + nano \ + htop \ + tree \ + unzip \ + zip \ + build-essential \ + software-properties-common \ + apt-transport-https \ + ca-certificates \ + gnupg \ + lsb-release + +echo "๐Ÿ Setting up Python environment..." +sudo apt install -y \ + python3 \ + python3-pip \ + python3-venv \ + python3-dev \ + python3-setuptools + +python3 -m pip install --user pipx +python3 -m pipx ensurepath + +echo "๐Ÿ“— Setting up Node.js..." +curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - +sudo apt install -y nodejs + +echo "๐Ÿงถ Installing Yarn..." +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt update && sudo apt install -y yarn + +echo "โ˜• Setting up Java..." +sudo apt install -y openjdk-17-jdk openjdk-17-jre + +echo "๐Ÿ”ต Setting up Go..." +GO_VERSION="1.21.4" +wget "https://golang.org/dl/go${GO_VERSION}.linux-amd64.tar.gz" +sudo rm -rf /usr/local/go +sudo tar -C /usr/local -xzf "go${GO_VERSION}.linux-amd64.tar.gz" +rm "go${GO_VERSION}.linux-amd64.tar.gz" + +echo "๐Ÿฆ€ Setting up Rust..." +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" + +echo "๐Ÿ’Ž Setting up Ruby..." +sudo apt install -y ruby-full + +echo "๐Ÿ˜ Setting up PHP..." +sudo apt install -y \ + php \ + php-cli \ + php-common \ + php-curl \ + php-json \ + php-mbstring \ + php-xml \ + php-zip + +curl -sS https://getcomposer.org/installer | php +sudo mv composer.phar /usr/local/bin/composer + +# Database tools +echo "๐Ÿ—„๏ธ Setting up database tools..." +sudo apt install -y \ + sqlite3 \ + postgresql-client \ + mysql-client + + + +# Final cleanup +echo "๐Ÿงน Cleaning up..." +sudo apt autoremove -y +sudo apt autoclean + diff --git a/nginx.conf b/nginx.conf index 6db257c..f5fe9e2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -25,14 +25,14 @@ http { proxy_set_header X-Forwarded-Proto $scheme; } - # Port forwarding - matches /space/8080, /space/3001, etc. - # Routes to localhost ports (containers running in Docker-in-Docker) - location ~ ^/space/(\d+)(/.*)?$ { + # Port forwarding - matches /8080/workspace, /3001/file.txt, etc. + # Routes to localhost ports (e.g., /44831/workspace -> localhost:44831/44831/workspace) + location ~ ^/(\d+)(/.*)?$ { set $port $1; - set $path $2; + set $fullpath $uri; set $target 127.0.0.1:$port; - proxy_pass http://$target$path; - proxy_set_header Host localhost; + proxy_pass http://$target$fullpath$is_args$args; + proxy_set_header Host localhost:$port; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; @@ -47,14 +47,45 @@ http { proxy_set_header Connection "upgrade"; # Additional headers for better compatibility - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Server $host; + proxy_set_header X-Forwarded-Host localhost:$port; + proxy_set_header X-Forwarded-Server localhost; + proxy_set_header X-Forwarded-Port $port; proxy_buffering off; # Handle authentication responses properly proxy_intercept_errors off; } + + # Port forwarding - matches /space/8080, /space/3001, etc. + # Routes to localhost ports (containers running in Docker-in-Docker) + location ~ ^/space/(\d+)(/.*)?$ { + set $port $1; + set $path $2; + set $target 127.0.0.1:$port; + proxy_pass http://$target$path$is_args$args; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Pass through authentication headers for HTTP Basic Auth + proxy_pass_header Authorization; + proxy_set_header Authorization $http_authorization; + + # WebSocket support for development servers + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + + # Additional headers for better compatibility + proxy_buffering off; + proxy_read_timeout 86400; + + # Handle authentication responses properly + proxy_intercept_errors off; + } + # Frontend fallback - must be last location / { proxy_pass http://frontend; proxy_set_header Host $host; @@ -66,5 +97,6 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } + } } \ No newline at end of file diff --git a/src/api/spaces/space.route.js b/src/api/spaces/space.route.js index e43f209..cbbbbf7 100644 --- a/src/api/spaces/space.route.js +++ b/src/api/spaces/space.route.js @@ -4,7 +4,8 @@ import { startContainer, stopContainer, getContainerStatus, - getUserSpaces + getUserSpaces, + deleteSpace } from "../../utils/spaces.js"; const router = express.Router(); @@ -84,4 +85,17 @@ router.get("/list", async (req, res) => { } }); +router.delete("/delete/:spaceId", async (req, res) => { + const { spaceId } = req.params; + const authorization = req.headers.authorization; + + try { + const result = await deleteSpace(spaceId, authorization); + res.json(result); + } catch (err) { + const statusCode = err.statusCode || (err.message.includes("required") || err.message.includes("Missing") || err.message.includes("Invalid authorization") ? 400 : 500); + res.status(statusCode).json({ error: err.message }); + } +}); + export default router; diff --git a/src/utils/spaces.js b/src/utils/spaces.js index 4a4b7de..6bc45fd 100644 --- a/src/utils/spaces.js +++ b/src/utils/spaces.js @@ -2,6 +2,12 @@ import Docker from "dockerode"; import getPort from "get-port"; import pg from "./db.js"; import { getUser } from "./user.js"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); const docker = new Docker(); @@ -9,7 +15,10 @@ const containerConfigs = { "code-server": { image: "linuxserver/code-server", port: "8443/tcp", - env: (password) => [`PASSWORD=${password}`], + env: (password, port) => [ + `PASSWORD=${password}`, + `DEFAULT_WORKSPACE=/config/workspace` + ], description: "VS Code Server" }, "blender": { @@ -56,15 +65,48 @@ export const createContainer = async (password, type, authorization) => { const container = await docker.createContainer({ Image: config.image, - Env: config.env(password), + Env: config.env(password, port), ExposedPorts: { [config.port]: {} }, HostConfig: { PortBindings: { [config.port]: [{ HostPort: `${port}` }] }, + NetworkMode: "bridge", + Dns: ["8.8.8.8", "8.8.4.4", "1.1.1.1"], + PublishAllPorts: false, + RestartPolicy: { Name: "unless-stopped" } }, }); await container.start(); + if (type.toLowerCase() === "code-server") { + try { + console.log("Running setup script for code-server container..."); + const setupScriptPath = path.join(__dirname, "../../code-server-setup.sh"); + const setupScript = fs.readFileSync(setupScriptPath, "utf8"); + + const exec = await container.exec({ + Cmd: ["bash", "-c", `cat > /tmp/setup.sh << 'EOF'\n${setupScript}\nEOF && chmod +x /tmp/setup.sh && /tmp/setup.sh`], + AttachStdout: true, + AttachStderr: true, + }); + + const stream = await exec.start(); + stream.pipe(process.stdout); + + console.log("Setup script executed successfully"); + } catch (setupErr) { + console.error("Failed to run setup script (container will still be created):", setupErr); + } + } + + if (process.env.DOCKER === 'false') { + console.log("Non-Docker environment detected, adjusting access URL"); + var access_url = `${process.env.SERVER_URL}:${port}`; + } else { + console.log("Docker environment detected, using standard access URL"); + var access_url = `${process.env.SERVER_URL}/space/${port}/`; + } + const [newSpace] = await pg('spaces') .insert({ user_id: user.id, @@ -73,13 +115,10 @@ export const createContainer = async (password, type, authorization) => { description: config.description, image: config.image, port, - access_url: `${process.env.SERVER_URL}/space/${port}/` + access_url: access_url }) .returning(['id', 'container_id', 'type', 'description', 'image', 'port', 'access_url']); - if (process.env.DOCKER === 'false') { - newSpace.access_url = `http://${process.env.SERVER_URL}:${newSpace.port}`; - } return { message: "Container created successfully", @@ -297,3 +336,63 @@ export const getSpacesByUserId = async (userId) => { throw new Error("Failed to get spaces for user"); } }; + +export const deleteSpace = async (spaceId, authorization) => { + if (!spaceId) { + throw new Error("Space ID is required"); + } + + if (!authorization) { + throw new Error("Missing authorization token"); + } + + const user = await getUser(authorization); + if (!user) { + throw new Error("Invalid authorization token"); + } + + try { + const space = await pg('spaces') + .where('id', spaceId) + .where('user_id', user.id) + .first(); + + if (!space) { + const error = new Error("Space not found or not owned by user"); + error.statusCode = 404; + throw error; + } + + const container = docker.getContainer(space.container_id); + + try { + await container.inspect(); + await container.stop(); + } catch (err) { + console.log("Container already stopped or doesn't exist, continuing with deletion"); + } + + try { + await container.remove(); + } catch (err) { + console.error("Failed to remove container:", err); + } + + await pg('spaces') + .where('id', spaceId) + .delete(); + + return { + message: "Space deleted successfully", + spaceId: space.id, + }; + } catch (err) { + console.error("Error deleting space:", err); + + if (err.statusCode === 404) { + throw err; + } + + throw new Error("Failed to delete space"); + } +} diff --git a/start.sh b/start.sh index b592112..d25fb28 100644 --- a/start.sh +++ b/start.sh @@ -1,5 +1,12 @@ #!/bin/bash +# Enable IP forwarding for nested containers +echo 1 > /proc/sys/net/ipv4/ip_forward + +# Configure iptables for NAT to allow nested containers internet access +iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 2>/dev/null || true +iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o docker0 -j MASQUERADE 2>/dev/null || true + if docker info >/dev/null 2>&1; then echo "Docker is available." else