spaces stuff

This commit is contained in:
Charmunks 2025-11-05 14:07:53 -05:00
parent a4815f03a1
commit 153ae3dec5
10 changed files with 312 additions and 20 deletions

3
.gitignore vendored
View file

@ -2,7 +2,8 @@
node_modules
dist
build
AGENTS.md
.github
.vscode
.DS_Store
.claude
.claude

View file

@ -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

View file

@ -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",

View file

@ -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.',

View file

@ -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 @@
>
</button>
<button
class="action-btn delete"
on:click={() => deleteSpace(space.id)}
>
Delete
</button>
{/if}
</div>
</div>

93
code-server-setup.sh Normal file
View file

@ -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

View file

@ -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";
}
}
}

View file

@ -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;

View file

@ -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");
}
}

View file

@ -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