feat: update README, enhance setup scripts, and add dev utilities for improved workflow
This commit is contained in:
@@ -1,111 +1,148 @@
|
|||||||
# 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
|
||||||
|
|
||||||
```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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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"]
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 "$@"
|
||||||
@@ -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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user