feat: update README, enhance setup scripts, and add dev utilities for improved workflow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
## Struttura
|
||||
@@ -8,104 +8,141 @@ Compose.
|
||||
```text
|
||||
backend/ FastAPI, MongoDB, mitmdump manager
|
||||
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
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
Lo script crea `backend/.env`, genera le credenziali mancanti e avvia lo stack
|
||||
Docker completo:
|
||||
`setup.sh` fa bootstrap della macchina/progetto:
|
||||
- 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
|
||||
Frontend http://localhost:5173
|
||||
Backend http://localhost:3671
|
||||
## Runtime reale / vulnbox
|
||||
|
||||
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
|
||||
takeover della porta di un servizio Docker locale, fai girare il backend sulla
|
||||
### Backend
|
||||
|
||||
```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.
|
||||
|
||||
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
|
||||
./setup.sh service
|
||||
./dev.sh mongo up
|
||||
./dev.sh mongo status
|
||||
./dev.sh mongo logs
|
||||
./dev.sh mongo down
|
||||
```
|
||||
|
||||
## Dev backend
|
||||
### Backend dev
|
||||
|
||||
```bash
|
||||
cp backend/.env.example backend/.env
|
||||
docker compose --env-file backend/.env up -d mongodb
|
||||
npm run dev:backend
|
||||
./dev.sh backend
|
||||
```
|
||||
|
||||
Docs FastAPI in dev: `http://localhost:3671/docs`.
|
||||
|
||||
## Test locale realistico
|
||||
|
||||
Per simulare il deployment reale, usa il backend sulla macchina host:
|
||||
### Frontend dev
|
||||
|
||||
```bash
|
||||
./setup.sh deps
|
||||
export SERVICE_SCAN_ROOT="$PWD"
|
||||
npm run dev:backend
|
||||
npm run dev:frontend
|
||||
./setup.sh playground
|
||||
./dev.sh frontend
|
||||
```
|
||||
|
||||
Poi:
|
||||
Questo avvia Vite normalmente sulla macchina host.
|
||||
|
||||
1. Apri `http://localhost:5173`.
|
||||
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/`.
|
||||
### Playground / servizi locali
|
||||
|
||||
Se il proxy è attivo, la route originale è ora posseduta da `mitmdump` e il
|
||||
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
|
||||
Per vedere i challenge/service disponibili:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
./dev.sh services list
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Backend:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
cd backend
|
||||
uv run ruff check .
|
||||
uv run pytest
|
||||
```
|
||||
|
||||
Equivalente a:
|
||||
Frontend:
|
||||
|
||||
```bash
|
||||
cd backend && uv run ruff check . && uv run pytest
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Per testare solo il playground:
|
||||
|
||||
```bash
|
||||
npm run playground:up
|
||||
npm run playground:down
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
## API principali
|
||||
@@ -121,7 +158,7 @@ npm run playground:down
|
||||
- `GET /proxy/{port}/logs`
|
||||
- `WS /proxy/{port}/logs/ws`
|
||||
- `GET /proxy/mitmproxy/options`
|
||||
- `GET /proxy/mitmproxy/examples`
|
||||
- `GET /proxy/mitmproxy/templates`
|
||||
|
||||
## 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.
|
||||
@@ -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
|
||||
container_name: theplug-frontend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
API_UPSTREAM: "host.docker.internal:${API_PORT:-3671}"
|
||||
ports:
|
||||
- "${FRONTEND_PORT:-5173}:80"
|
||||
depends_on:
|
||||
- backend
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
mongodb:
|
||||
image: mongo:8.2
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ RUN npm run build
|
||||
|
||||
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
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ server {
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3671/;
|
||||
proxy_pass http://${API_UPSTREAM}/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_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";
|
||||
|
||||
@@ -11,6 +11,31 @@ type LogPlaneOptions = {
|
||||
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({
|
||||
token,
|
||||
selectedPort,
|
||||
@@ -21,20 +46,63 @@ export function useControlPlaneLogs({
|
||||
const logFilter = ref("");
|
||||
const logsPaused = ref(false);
|
||||
const logsCleared = ref(false);
|
||||
const logLines = shallowRef<string[]>([]);
|
||||
|
||||
let logSocket: WebSocket | null = null;
|
||||
let logReconnectTimer: number | null = null;
|
||||
let logFlushTimer: number | null = null;
|
||||
let pendingLogChunk = "";
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
const source = logs.value || "";
|
||||
const query = logFilter.value.trim().toLowerCase();
|
||||
if (!query) return source;
|
||||
return source
|
||||
.split("\n")
|
||||
if (!query) return logs.value;
|
||||
return logLines.value
|
||||
.filter((line) => line.toLowerCase().includes(query))
|
||||
.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 {
|
||||
try {
|
||||
const payload = JSON.parse(raw);
|
||||
@@ -55,7 +123,7 @@ export function useControlPlaneLogs({
|
||||
if (port === null) return;
|
||||
try {
|
||||
if (!logsCleared.value) {
|
||||
logs.value = (await api.logs(port)).log;
|
||||
replaceLogs((await api.logs(port)).log);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!silent) onError(error);
|
||||
@@ -72,7 +140,7 @@ export function useControlPlaneLogs({
|
||||
logReconnectTimer = null;
|
||||
if (!token.value || logsPaused.value || selectedPort.value !== proxyPort) return;
|
||||
void openLogStream(proxyPort);
|
||||
}, 1200);
|
||||
}, LOG_RECONNECT_DELAY_MS);
|
||||
}
|
||||
|
||||
async function openLogStream(proxyPort: number) {
|
||||
@@ -98,10 +166,13 @@ export function useControlPlaneLogs({
|
||||
if (frame === null) return;
|
||||
if (frame.type === "snapshot") {
|
||||
if (logsCleared.value) return;
|
||||
logs.value = frame.log;
|
||||
resetLogBuffer();
|
||||
replaceLogs(frame.log);
|
||||
return;
|
||||
}
|
||||
logs.value += frame.log;
|
||||
|
||||
pendingLogChunk += frame.log;
|
||||
scheduleLogFlush();
|
||||
};
|
||||
|
||||
socket.onclose = (event) => {
|
||||
@@ -131,6 +202,8 @@ export function useControlPlaneLogs({
|
||||
window.clearTimeout(logReconnectTimer);
|
||||
logReconnectTimer = null;
|
||||
}
|
||||
stopPendingFlush();
|
||||
pendingLogChunk = "";
|
||||
if (logSocket === null) return;
|
||||
logSocket.onclose = null;
|
||||
logSocket.close();
|
||||
@@ -142,13 +215,13 @@ export function useControlPlaneLogs({
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs.value = "";
|
||||
resetLogBuffer();
|
||||
logsCleared.value = true;
|
||||
}
|
||||
|
||||
function resetLogs() {
|
||||
stopLogStream();
|
||||
logs.value = "";
|
||||
resetLogBuffer();
|
||||
logFilter.value = "";
|
||||
logsPaused.value = false;
|
||||
logsCleared.value = false;
|
||||
@@ -158,7 +231,7 @@ export function useControlPlaneLogs({
|
||||
[selectedPort, token, logsPaused],
|
||||
([port, authToken, paused], [previousPort]) => {
|
||||
if (port !== previousPort) {
|
||||
logs.value = "";
|
||||
resetLogBuffer();
|
||||
logFilter.value = "";
|
||||
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; }
|
||||
|
||||
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() {
|
||||
docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/docker-compose.yml" "$@"
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
|
||||
}
|
||||
|
||||
playground() {
|
||||
docker compose -f "$ROOT_DIR/playground/docker-compose.yml" "$@"
|
||||
ensure_uv() {
|
||||
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() {
|
||||
@@ -35,11 +50,12 @@ sync_env_values() {
|
||||
|
||||
ensure_env() {
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
sync_env_values
|
||||
return
|
||||
fi
|
||||
|
||||
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
|
||||
mongo_pass=$(openssl rand -hex 8)
|
||||
@@ -56,74 +72,58 @@ ensure_env() {
|
||||
info "AUTH_PASSWORD: $auth_pass"
|
||||
}
|
||||
|
||||
install_service() {
|
||||
mkdir -p ~/.config/systemd/user
|
||||
cat > ~/.config/systemd/user/theplug.service <<EOF
|
||||
[Unit]
|
||||
Description=thePlug stack
|
||||
After=network.target
|
||||
prepare_backend() {
|
||||
info "Installing backend dependencies..."
|
||||
(
|
||||
cd "$BACKEND_DIR"
|
||||
uv sync --python 3.14
|
||||
)
|
||||
}
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=$ROOT_DIR
|
||||
ExecStart=/usr/bin/docker compose --env-file $ENV_FILE -f $ROOT_DIR/docker-compose.yml up -d
|
||||
ExecStop=/usr/bin/docker compose --env-file $ENV_FILE -f $ROOT_DIR/docker-compose.yml down
|
||||
prepare_frontend() {
|
||||
require_command npm
|
||||
info "Installing frontend dependencies..."
|
||||
(
|
||||
cd "$FRONTEND_DIR"
|
||||
npm install
|
||||
npm run build
|
||||
)
|
||||
}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
prepare_docker_assets() {
|
||||
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
|
||||
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
|
||||
sync_env_values
|
||||
info "Building and starting backend, frontend and MongoDB..."
|
||||
compose up -d --build
|
||||
info "Backend: http://localhost:$(grep -E '^API_PORT=' "$ENV_FILE" | cut -d '=' -f2 || echo 3671)"
|
||||
info "Frontend: http://localhost:$(grep -E '^FRONTEND_PORT=' "$ENV_FILE" | cut -d '=' -f2 || echo 5173)"
|
||||
prepare_backend
|
||||
prepare_frontend
|
||||
prepare_docker_assets
|
||||
print_next_steps
|
||||
}
|
||||
|
||||
start_deps() {
|
||||
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
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user