feat: update README, enhance setup scripts, and add dev utilities for improved workflow

This commit is contained in:
gabdevele
2026-05-07 18:38:19 +02:00 Unverified
parent 9d6efe5e60
commit 7dbe698a49
11 changed files with 579 additions and 198 deletions
+96 -59
View File
@@ -1,6 +1,6 @@
# thePlug # thePlug
Wrapper web per gestire proxy `mitmdump` davanti a servizi esposti via Docker Web UI per gestire proxy `mitmdump` davanti a servizi esposti via Docker
Compose. Compose.
## Struttura ## Struttura
@@ -8,104 +8,141 @@ Compose.
```text ```text
backend/ FastAPI, MongoDB, mitmdump manager backend/ FastAPI, MongoDB, mitmdump manager
frontend/ Vue 3, Vite, CodeMirror + Python LSP frontend/ Vue 3, Vite, CodeMirror + Python LSP
playground/ servizio locale minimale per test end-to-end playground/ challenge/service compose locali
templates/ template proxy YAML file-backed
``` ```
## Setup rapido ## Setup iniziale
```bash ```bash
./setup.sh ./setup.sh
``` ```
Lo script crea `backend/.env`, genera le credenziali mancanti e avvia lo stack `setup.sh` fa bootstrap della macchina/progetto:
Docker completo: - installa `uv` se manca
- installa Python `3.14` via `uv`
- crea `backend/.env` se manca
- installa dipendenze backend e frontend
- builda il frontend
- prepara MongoDB e l'immagine Docker del frontend
```text ## Runtime reale / vulnbox
Frontend http://localhost:5173
Backend http://localhost:3671 Questi sono i comandi pensati per l’ambiente reale di gara.
### MongoDB
```bash
./run.sh mongo up
./run.sh mongo status
./run.sh mongo logs
./run.sh mongo down
``` ```
Questa modalità è buona per smoke test UI/API. Se vuoi provare davvero il ### Backend
takeover della porta di un servizio Docker locale, fai girare il backend sulla
```bash
./run.sh backend
```
### Frontend Docker
```bash
./run.sh frontend up
./run.sh frontend status
./run.sh frontend logs
./run.sh frontend down
```
Il frontend in container fa proxy API verso il backend in esecuzione sulla
macchina host. macchina host.
Guida completa: [TESTING.md](/home/gabdevele/CTF/thePlug/TESTING.md) ## Workflow development
Per installare anche il servizio systemd user: Questi comandi sono pensati per sviluppo locale, test e playground.
### MongoDB
```bash ```bash
./setup.sh service ./dev.sh mongo up
./dev.sh mongo status
./dev.sh mongo logs
./dev.sh mongo down
``` ```
## Dev backend ### Backend dev
```bash ```bash
cp backend/.env.example backend/.env ./dev.sh backend
docker compose --env-file backend/.env up -d mongodb
npm run dev:backend
``` ```
Docs FastAPI in dev: `http://localhost:3671/docs`. Docs FastAPI in dev: `http://localhost:3671/docs`.
## Test locale realistico ### Frontend dev
Per simulare il deployment reale, usa il backend sulla macchina host:
```bash ```bash
./setup.sh deps ./dev.sh frontend
export SERVICE_SCAN_ROOT="$PWD"
npm run dev:backend
npm run dev:frontend
./setup.sh playground
``` ```
Poi: Questo avvia Vite normalmente sulla macchina host.
1. Apri `http://localhost:5173`. ### Playground / servizi locali
2. Fai login con le credenziali in `backend/.env`.
3. Crea un nuovo proxy.
4. Seleziona il servizio `echo` rilevato dal backend sotto la cartella `playground`.
5. Lascia `mode=reverse`.
6. Verifica che `proxy_port` combaci con la porta del servizio (`9081`) se vuoi che il proxy prenda davvero il posto della route originale.
7. Salva e premi `Start`.
8. Esegui `curl http://127.0.0.1:9081/`.
Se il proxy è attivo, la route originale è ora posseduta da `mitmdump` e il Per vedere i challenge/service disponibili:
servizio reale è stato spostato su una porta upstream temporanea, visibile nella
UI e nello stato runtime.
`SERVICE_SCAN_ROOT` controls where the backend looks for services. The scan is
intentionally shallow: it inspects only the immediate child directories of that
root, which matches the expected host layout.
## Dev frontend
```bash ```bash
cd frontend ./dev.sh services list
npm install
npm run dev
``` ```
Il dev server usa `/api` come proxy verso `http://127.0.0.1:3671`. Per avviarli o fermarli:
```bash
./dev.sh services up GabibbiTowers
./dev.sh services down GabibbiTowers
./dev.sh services up all
./dev.sh services status all
./dev.sh services down all
```
`dev.sh` scopre automaticamente i compose da `playground/*/docker-compose.yml`.
## Flussi consigliati
Runtime reale:
```bash
./setup.sh
./run.sh mongo up
./run.sh backend
./run.sh frontend up
```
Sviluppo locale:
```bash
./setup.sh
./dev.sh mongo up
./dev.sh backend
./dev.sh frontend
./dev.sh services up <challenge>
```
## Test ## Test
Backend:
```bash ```bash
npm run test cd backend
uv run ruff check .
uv run pytest
``` ```
Equivalente a: Frontend:
```bash ```bash
cd backend && uv run ruff check . && uv run pytest cd frontend
cd frontend && npm run build npm run build
```
Per testare solo il playground:
```bash
npm run playground:up
npm run playground:down
``` ```
## API principali ## API principali
@@ -121,7 +158,7 @@ npm run playground:down
- `GET /proxy/{port}/logs` - `GET /proxy/{port}/logs`
- `WS /proxy/{port}/logs/ws` - `WS /proxy/{port}/logs/ws`
- `GET /proxy/mitmproxy/options` - `GET /proxy/mitmproxy/options`
- `GET /proxy/mitmproxy/examples` - `GET /proxy/mitmproxy/templates`
## Riferimenti ## Riferimenti
-6
View File
@@ -1,7 +1 @@
- con tante richieste contemporaneamente la pagina inizia a laggare tantissimo per colpa dei log, quindi bisogna capire
come ottimizzare.
- rimuovere package.json
- sistemare setup.sh per poter avviare il server in modo più semplice
- creare un altro script bash per runnare i vari servizi in maniera semplice.
- aggiornare readme
- fixare i test di unità, di integrazione e aggiungere test reali di benchmark. - fixare i test di unità, di integrazione e aggiungere test reali di benchmark.
-26
View File
@@ -1,26 +0,0 @@
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app/src
WORKDIR /app
COPY pyproject.toml ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir \
"authlib>=1.6.0" \
"basedpyright>=1.39.0" \
"fastapi[standard]>=0.135.3" \
"mitmproxy>=12.2.2" \
"motor>=3.7.1" \
"pwdlib[argon2,bcrypt]>=0.3.0" \
"pyyaml>=6.0.3" \
"pydantic-settings>=2.13.1" \
"ruff>=0.15.10"
COPY app ./src
EXPOSE 3671
CMD ["fastapi", "run", "src/main.py", "--host", "0.0.0.0", "--port", "3671"]
Executable
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; }
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"
PLAYGROUND_DIR="$ROOT_DIR/playground"
ENV_FILE="$BACKEND_DIR/.env"
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
require_env() {
[[ -f "$ENV_FILE" ]] || die "Missing $ENV_FILE. Run ./setup.sh first."
}
env_value() {
local key="$1"
local default_value="${2:-}"
local value
value=$(grep -E "^${key}=" "$ENV_FILE" | cut -d '=' -f2- || true)
if [[ -n "$value" ]]; then
printf '%s\n' "$value"
return
fi
printf '%s\n' "$default_value"
}
compose() {
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@"
}
service_compose_path() {
local name="$1"
local path="$PLAYGROUND_DIR/$name/docker-compose.yml"
[[ -f "$path" ]] || die "Unknown service group: $name"
printf '%s\n' "$path"
}
service_names() {
find "$PLAYGROUND_DIR" -mindepth 2 -maxdepth 2 -name docker-compose.yml -printf '%h\n' \
| xargs -r -n1 basename \
| sort
}
run_backend() {
require_command uv
require_env
local api_port
api_port=$(env_value API_PORT 3671)
info "Starting backend dev server on http://0.0.0.0:$api_port"
(
cd "$BACKEND_DIR"
uv run fastapi dev app/main.py --host 0.0.0.0 --port "$api_port"
)
}
run_frontend() {
require_command npm
require_env
info "Starting frontend Vite dev server"
(
cd "$FRONTEND_DIR"
npm run dev
)
}
run_mongo() {
require_command docker
require_env
local action="${1:-status}"
case "$action" in
up)
compose up -d mongodb
;;
down)
compose stop mongodb
;;
status)
compose ps mongodb
;;
logs)
compose logs -f mongodb
;;
*)
die "Unknown mongo action: $action"
;;
esac
}
run_services() {
require_command docker
local action="${1:-list}"
shift || true
case "$action" in
list)
service_names
;;
up|down|status)
local target="${1:-all}"
if [[ "$target" == "all" ]]; then
while IFS= read -r name; do
[[ -n "$name" ]] || continue
local compose_file
compose_file=$(service_compose_path "$name")
case "$action" in
up) docker compose -f "$compose_file" up -d ;;
down) docker compose -f "$compose_file" down ;;
status) docker compose -f "$compose_file" ps ;;
esac
done < <(service_names)
return
fi
local compose_file
compose_file=$(service_compose_path "$target")
case "$action" in
up) docker compose -f "$compose_file" up -d ;;
down) docker compose -f "$compose_file" down ;;
status) docker compose -f "$compose_file" ps ;;
esac
;;
*)
die "Unknown services action: $action"
;;
esac
}
show_help() {
cat <<'EOF'
Usage:
./dev.sh backend
./dev.sh frontend
./dev.sh mongo [up|down|status|logs]
./dev.sh services [list|up|down|status] [name|all]
Examples:
./dev.sh mongo up
./dev.sh backend
./dev.sh frontend
./dev.sh services list
./dev.sh services up GabibbiTowers
EOF
}
main() {
local command="${1:-help}"
shift || true
case "$command" in
backend)
run_backend "$@"
;;
frontend)
run_frontend "$@"
;;
mongo)
run_mongo "$@"
;;
services)
run_services "$@"
;;
help|-h|--help)
show_help
;;
*)
die "Unknown command: $command"
;;
esac
}
main "$@"
+4 -2
View File
@@ -4,10 +4,12 @@ services:
context: ./frontend context: ./frontend
container_name: theplug-frontend container_name: theplug-frontend
restart: unless-stopped restart: unless-stopped
environment:
API_UPSTREAM: "host.docker.internal:${API_PORT:-3671}"
ports: ports:
- "${FRONTEND_PORT:-5173}:80" - "${FRONTEND_PORT:-5173}:80"
depends_on: extra_hosts:
- backend - "host.docker.internal:host-gateway"
mongodb: mongodb:
image: mongo:8.2 image: mongo:8.2
+1 -1
View File
@@ -10,7 +10,7 @@ RUN npm run build
FROM nginx:1.29-alpine FROM nginx:1.29-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80 EXPOSE 80
+1 -1
View File
@@ -6,7 +6,7 @@ server {
index index.html; index index.html;
location /api/ { location /api/ {
proxy_pass http://backend:3671/; proxy_pass http://${API_UPSTREAM}/;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
+85 -12
View File
@@ -1,4 +1,4 @@
import { computed, ref, watch, type Ref } from "vue"; import { computed, ref, shallowRef, watch, type Ref } from "vue";
import { api, buildSocketUrl } from "../api"; import { api, buildSocketUrl } from "../api";
@@ -11,6 +11,31 @@ type LogPlaneOptions = {
onError: (error: unknown) => void; onError: (error: unknown) => void;
}; };
const LOG_RECONNECT_DELAY_MS = 1200;
const LOG_FLUSH_DELAY_MS = 75;
const MAX_LOG_LINES = 2500;
const MAX_LOG_CHARS = 250_000;
function splitLogText(text: string) {
return text === "" ? [] : text.split("\n");
}
function trimLogLines(lines: string[]) {
let next = lines.length > MAX_LOG_LINES ? lines.slice(-MAX_LOG_LINES) : lines;
let totalChars = next.reduce(
(count, line, index) => count + line.length + (index > 0 ? 1 : 0),
0,
);
while (totalChars > MAX_LOG_CHARS && next.length > 1) {
const [removed = ""] = next;
next = next.slice(1);
totalChars -= removed.length + 1;
}
return next;
}
export function useControlPlaneLogs({ export function useControlPlaneLogs({
token, token,
selectedPort, selectedPort,
@@ -21,20 +46,63 @@ export function useControlPlaneLogs({
const logFilter = ref(""); const logFilter = ref("");
const logsPaused = ref(false); const logsPaused = ref(false);
const logsCleared = ref(false); const logsCleared = ref(false);
const logLines = shallowRef<string[]>([]);
let logSocket: WebSocket | null = null; let logSocket: WebSocket | null = null;
let logReconnectTimer: number | null = null; let logReconnectTimer: number | null = null;
let logFlushTimer: number | null = null;
let pendingLogChunk = "";
const filteredLogs = computed(() => { const filteredLogs = computed(() => {
const source = logs.value || "";
const query = logFilter.value.trim().toLowerCase(); const query = logFilter.value.trim().toLowerCase();
if (!query) return source; if (!query) return logs.value;
return source return logLines.value
.split("\n")
.filter((line) => line.toLowerCase().includes(query)) .filter((line) => line.toLowerCase().includes(query))
.join("\n"); .join("\n");
}); });
function commitLogLines(lines: string[]) {
const trimmed = trimLogLines(lines);
logLines.value = trimmed;
logs.value = trimmed.join("\n");
}
function replaceLogs(text: string) {
commitLogLines(splitLogText(text));
}
function appendLogs(text: string) {
if (!text) return;
commitLogLines([...logLines.value, ...splitLogText(text)]);
}
function flushPendingLogs() {
if (!pendingLogChunk) return;
appendLogs(pendingLogChunk);
pendingLogChunk = "";
}
function stopPendingFlush() {
if (typeof window === "undefined" || logFlushTimer === null) return;
window.clearTimeout(logFlushTimer);
logFlushTimer = null;
}
function scheduleLogFlush() {
if (typeof window === "undefined" || logFlushTimer !== null) return;
logFlushTimer = window.setTimeout(() => {
logFlushTimer = null;
flushPendingLogs();
}, LOG_FLUSH_DELAY_MS);
}
function resetLogBuffer() {
stopPendingFlush();
pendingLogChunk = "";
logLines.value = [];
logs.value = "";
}
function parseLogFrame(raw: string): LogFrame | null { function parseLogFrame(raw: string): LogFrame | null {
try { try {
const payload = JSON.parse(raw); const payload = JSON.parse(raw);
@@ -55,7 +123,7 @@ export function useControlPlaneLogs({
if (port === null) return; if (port === null) return;
try { try {
if (!logsCleared.value) { if (!logsCleared.value) {
logs.value = (await api.logs(port)).log; replaceLogs((await api.logs(port)).log);
} }
} catch (error) { } catch (error) {
if (!silent) onError(error); if (!silent) onError(error);
@@ -72,7 +140,7 @@ export function useControlPlaneLogs({
logReconnectTimer = null; logReconnectTimer = null;
if (!token.value || logsPaused.value || selectedPort.value !== proxyPort) return; if (!token.value || logsPaused.value || selectedPort.value !== proxyPort) return;
void openLogStream(proxyPort); void openLogStream(proxyPort);
}, 1200); }, LOG_RECONNECT_DELAY_MS);
} }
async function openLogStream(proxyPort: number) { async function openLogStream(proxyPort: number) {
@@ -98,10 +166,13 @@ export function useControlPlaneLogs({
if (frame === null) return; if (frame === null) return;
if (frame.type === "snapshot") { if (frame.type === "snapshot") {
if (logsCleared.value) return; if (logsCleared.value) return;
logs.value = frame.log; resetLogBuffer();
replaceLogs(frame.log);
return; return;
} }
logs.value += frame.log;
pendingLogChunk += frame.log;
scheduleLogFlush();
}; };
socket.onclose = (event) => { socket.onclose = (event) => {
@@ -131,6 +202,8 @@ export function useControlPlaneLogs({
window.clearTimeout(logReconnectTimer); window.clearTimeout(logReconnectTimer);
logReconnectTimer = null; logReconnectTimer = null;
} }
stopPendingFlush();
pendingLogChunk = "";
if (logSocket === null) return; if (logSocket === null) return;
logSocket.onclose = null; logSocket.onclose = null;
logSocket.close(); logSocket.close();
@@ -142,13 +215,13 @@ export function useControlPlaneLogs({
} }
function clearLogs() { function clearLogs() {
logs.value = ""; resetLogBuffer();
logsCleared.value = true; logsCleared.value = true;
} }
function resetLogs() { function resetLogs() {
stopLogStream(); stopLogStream();
logs.value = ""; resetLogBuffer();
logFilter.value = ""; logFilter.value = "";
logsPaused.value = false; logsPaused.value = false;
logsCleared.value = false; logsCleared.value = false;
@@ -158,7 +231,7 @@ export function useControlPlaneLogs({
[selectedPort, token, logsPaused], [selectedPort, token, logsPaused],
([port, authToken, paused], [previousPort]) => { ([port, authToken, paused], [previousPort]) => {
if (port !== previousPort) { if (port !== previousPort) {
logs.value = ""; resetLogBuffer();
logFilter.value = ""; logFilter.value = "";
logsCleared.value = false; logsCleared.value = false;
} }
-19
View File
@@ -1,19 +0,0 @@
{
"name": "theplug",
"private": true,
"version": "0.1.0",
"scripts": {
"dev:backend": "cd backend && PYTHONPATH=. uv run fastapi dev app/main.py --host 0.0.0.0 --port 3671",
"dev:frontend": "cd frontend && npm run dev",
"dev:deps": "./setup.sh deps",
"test:backend": "cd backend && uv run pytest",
"lint:backend": "cd backend && uv run ruff check .",
"format:backend": "cd backend && uv run ruff format .",
"build:frontend": "cd frontend && npm run build",
"test": "npm run lint:backend && npm run test:backend && npm run build:frontend",
"compose:up": "./setup.sh",
"compose:down": "docker compose --env-file backend/.env -f docker-compose.yml down",
"playground:up": "docker compose -f playground/docker-compose.yml up -d",
"playground:down": "docker compose -f playground/docker-compose.yml down"
}
}
Executable
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; }
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_DIR="$ROOT_DIR/backend"
ENV_FILE="$BACKEND_DIR/.env"
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
require_env() {
[[ -f "$ENV_FILE" ]] || die "Missing $ENV_FILE. Run ./setup.sh first."
}
env_value() {
local key="$1"
local default_value="${2:-}"
local value
value=$(grep -E "^${key}=" "$ENV_FILE" | cut -d '=' -f2- || true)
if [[ -n "$value" ]]; then
printf '%s\n' "$value"
return
fi
printf '%s\n' "$default_value"
}
compose() {
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@"
}
run_backend() {
require_command uv
require_env
local api_port
api_port=$(env_value API_PORT 3671)
info "Starting backend on http://0.0.0.0:$api_port"
(
cd "$BACKEND_DIR"
uv run fastapi run app/main.py --host 0.0.0.0 --port "$api_port"
)
}
run_frontend() {
require_command docker
require_env
local action="${1:-up}"
case "$action" in
up)
compose up -d frontend
info "Frontend container running on http://localhost:$(env_value FRONTEND_PORT 5173)"
;;
down)
compose stop frontend
;;
status)
compose ps frontend
;;
logs)
compose logs -f frontend
;;
*)
die "Unknown frontend action: $action"
;;
esac
}
run_mongo() {
require_command docker
require_env
local action="${1:-status}"
case "$action" in
up)
compose up -d mongodb
info "MongoDB running on mongodb://localhost:27017"
;;
down)
compose stop mongodb
;;
status)
compose ps mongodb
;;
logs)
compose logs -f mongodb
;;
*)
die "Unknown mongo action: $action"
;;
esac
}
show_help() {
cat <<'EOF'
Usage:
./run.sh backend
./run.sh frontend [up|down|status|logs]
./run.sh mongo [up|down|status|logs]
Examples:
./run.sh mongo up
./run.sh backend
./run.sh frontend up
./run.sh frontend logs
EOF
}
main() {
local command="${1:-help}"
shift || true
case "$command" in
backend)
run_backend "$@"
;;
frontend)
run_frontend "$@"
;;
mongo)
run_mongo "$@"
;;
help|-h|--help)
show_help
;;
*)
die "Unknown command: $command"
;;
esac
}
main "$@"
+69 -69
View File
@@ -11,14 +11,29 @@ warn() { echo -e "${YELLOW}[!]${NC} $*"; }
die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; } die() { echo -e "${RED}[x] FATAL:${NC} $*" >&2; exit 1; }
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$ROOT_DIR/backend/.env" BACKEND_DIR="$ROOT_DIR/backend"
FRONTEND_DIR="$ROOT_DIR/frontend"
ENV_FILE="$BACKEND_DIR/.env"
compose() { require_command() {
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@" command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
} }
playground() { ensure_uv() {
docker compose -f "$ROOT_DIR/playground/docker-compose.yml" "$@" if command -v uv >/dev/null 2>&1; then
return
fi
info "Installing uv..."
require_command curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
command -v uv >/dev/null 2>&1 || die "uv installed but not found in PATH"
}
ensure_python() {
info "Ensuring Python 3.14 is available via uv..."
uv python install 3.14
} }
sync_env_values() { sync_env_values() {
@@ -35,11 +50,12 @@ sync_env_values() {
ensure_env() { ensure_env() {
if [[ -f "$ENV_FILE" ]]; then if [[ -f "$ENV_FILE" ]]; then
sync_env_values
return return
fi fi
warn "backend/.env not found, creating it from backend/.env.example" warn "backend/.env not found, creating it from backend/.env.example"
cp "$ROOT_DIR/backend/.env.example" "$ENV_FILE" cp "$BACKEND_DIR/.env.example" "$ENV_FILE"
local mongo_pass jwt_secret auth_pass local mongo_pass jwt_secret auth_pass
mongo_pass=$(openssl rand -hex 8) mongo_pass=$(openssl rand -hex 8)
@@ -56,74 +72,58 @@ ensure_env() {
info "AUTH_PASSWORD: $auth_pass" info "AUTH_PASSWORD: $auth_pass"
} }
install_service() { prepare_backend() {
mkdir -p ~/.config/systemd/user info "Installing backend dependencies..."
cat > ~/.config/systemd/user/theplug.service <<EOF (
[Unit] cd "$BACKEND_DIR"
Description=thePlug stack uv sync --python 3.14
After=network.target )
}
[Service] prepare_frontend() {
Type=oneshot require_command npm
RemainAfterExit=yes info "Installing frontend dependencies..."
WorkingDirectory=$ROOT_DIR (
ExecStart=/usr/bin/docker compose --env-file $ENV_FILE -f $ROOT_DIR/docker-compose.yml up -d cd "$FRONTEND_DIR"
ExecStop=/usr/bin/docker compose --env-file $ENV_FILE -f $ROOT_DIR/docker-compose.yml down npm install
npm run build
)
}
[Install] prepare_docker_assets() {
WantedBy=default.target require_command docker
info "Preparing Docker assets..."
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" pull mongodb
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" build frontend
}
print_next_steps() {
cat <<EOF
Setup complete.
Next commands:
./run.sh mongo up
./run.sh backend
./run.sh frontend up
Optional:
./dev.sh mongo up
./dev.sh backend
./dev.sh frontend
./dev.sh services list
EOF EOF
systemctl --user daemon-reload
systemctl --user enable theplug
info "systemd user service installed"
} }
start_stack() { main() {
require_command openssl
ensure_uv
ensure_python
ensure_env ensure_env
sync_env_values prepare_backend
info "Building and starting backend, frontend and MongoDB..." prepare_frontend
compose up -d --build prepare_docker_assets
info "Backend: http://localhost:$(grep -E '^API_PORT=' "$ENV_FILE" | cut -d '=' -f2 || echo 3671)" print_next_steps
info "Frontend: http://localhost:$(grep -E '^FRONTEND_PORT=' "$ENV_FILE" | cut -d '=' -f2 || echo 5173)"
} }
start_deps() { main "$@"
ensure_env
sync_env_values
info "Starting MongoDB for host-backend testing..."
compose up -d mongodb
info "Now run these on the host:"
info " npm run dev:backend"
info " npm run dev:frontend"
}
start_playground() {
info "Starting local playground upstream service..."
playground up -d
info "Playground service: http://127.0.0.1:9081"
}
if [[ "${1:-}" == "service" ]]; then
ensure_env
install_service
systemctl --user start theplug
exit 0
fi
if [[ "${1:-}" == "deps" ]]; then
start_deps
exit 0
fi
if [[ "${1:-}" == "playground" ]]; then
start_playground
exit 0
fi
if [[ -d "/srv/theplug" && "$ROOT_DIR" != "/srv/theplug" ]]; then
info "thePlug found in /srv/theplug, switching there"
cd /srv/theplug
exec ./setup.sh "$@"
fi
start_stack