mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-07-31 17:27:11 -04:00
323 lines
13 KiB
Bash
Executable File
323 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ----------------------------------------------------------------------------
|
|
# AliasVault dev app starter.
|
|
#
|
|
# Use this to start AliasVault apps in dev mode with a preconfigured port
|
|
# setup: every service runs from source on a single, consistent set of ports,
|
|
# so the apps always know where to find each other.
|
|
#
|
|
# Usage:
|
|
# ./scripts/dev.sh # interactive menu (pick an app)
|
|
# ./scripts/dev.sh <app> # start one app: api | client | admin | smtp | taskrunner | ext | mobile
|
|
# ./scripts/dev.sh db-start # start (only) the dev database
|
|
# ./scripts/dev.sh db-stop # stop & remove this instance's dev database
|
|
# ./scripts/dev.sh ports # print the resolved port map and exit
|
|
# ----------------------------------------------------------------------------
|
|
set -euo pipefail
|
|
|
|
# --- Resolve repo paths --------------------------------------------------------
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
cd "$ROOT_DIR"
|
|
|
|
# --- Colors (no-op if not a TTY) ----------------------------------------------
|
|
if [ -t 1 ]; then
|
|
BOLD=$'\033[1m'; DIM=$'\033[2m'; GREEN=$'\033[32m'; CYAN=$'\033[36m'
|
|
YELLOW=$'\033[33m'; RED=$'\033[31m'; NC=$'\033[0m'
|
|
else
|
|
BOLD=""; DIM=""; GREEN=""; CYAN=""; YELLOW=""; RED=""; NC=""
|
|
fi
|
|
info() { printf "%s==>%s %s\n" "$CYAN" "$NC" "$1"; }
|
|
warn() { printf "%s! %s%s\n" "$YELLOW" "$1" "$NC"; }
|
|
die() { printf "%sError:%s %s\n" "$RED" "$NC" "$1" >&2; exit 1; }
|
|
|
|
LOCAL_ENV="$ROOT_DIR/dev.env"
|
|
|
|
# --- Config defaults -----------------------------------------------------------
|
|
DEFAULT_BASE_PORT=5100 # first port of the block; every service counts up from here
|
|
DEFAULT_PORT_STRIDE=10 # how far apart consecutive instances are spaced
|
|
|
|
# Write the dev.env template, with AV_INSTANCE pinned to the given value.
|
|
seed_local_env() {
|
|
cat > "$LOCAL_ENV" <<ENV
|
|
# ----------------------------------------------------------------------------
|
|
# AliasVault dev instance config — generated by scripts/dev.sh, gitignored.
|
|
#
|
|
# AV_INSTANCE picks which block of ports this machine uses. Bump it if the
|
|
# default ports are already taken on your machine (another stack, another dev,
|
|
# something else on those ports). Each step moves every port up by ${DEFAULT_PORT_STRIDE}:
|
|
#
|
|
# AV_INSTANCE=0 -> ${DEFAULT_BASE_PORT}..$((DEFAULT_BASE_PORT + 8)) (default)
|
|
# AV_INSTANCE=1 -> $((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE + 8))
|
|
# AV_INSTANCE=2 -> $((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE + 8))
|
|
# ...
|
|
# ----------------------------------------------------------------------------
|
|
AV_INSTANCE=$1
|
|
|
|
AV_BASE_PORT=$DEFAULT_BASE_PORT
|
|
AV_PORT_STRIDE=$DEFAULT_PORT_STRIDE
|
|
|
|
# UseDebugEncryptionKey for the Blazor client. 'true' uses a fixed debug
|
|
# encryption key so local logins skip the slow key derivation, speeds up dev.
|
|
# Note: this can conflict with some things like password verification steps.
|
|
# Switch to 'false' to use the actual user unlock flow compatible with production.
|
|
AV_USE_DEBUG_ENCRYPTION_KEY=true
|
|
ENV
|
|
}
|
|
|
|
# --- Load port config ----------------------------------------------------------
|
|
# First run: create a dev.env set to instance 0.
|
|
if [ ! -f "$LOCAL_ENV" ]; then
|
|
seed_local_env 0
|
|
info "Created dev.env (instance 0). Bump AV_INSTANCE there if the default ports are in use."
|
|
fi
|
|
|
|
# shellcheck disable=SC1091
|
|
set -a
|
|
. "$LOCAL_ENV"
|
|
set +a
|
|
|
|
# Precedence: dev.env > built-in defaults.
|
|
AV_INSTANCE="${AV_INSTANCE:-0}"
|
|
AV_BASE_PORT="${AV_BASE_PORT:-$DEFAULT_BASE_PORT}"
|
|
AV_PORT_STRIDE="${AV_PORT_STRIDE:-$DEFAULT_PORT_STRIDE}"
|
|
# Default true for pre-existing dev.env files that predate this setting.
|
|
AV_USE_DEBUG_ENCRYPTION_KEY="${AV_USE_DEBUG_ENCRYPTION_KEY:-true}"
|
|
case "$AV_INSTANCE" in (*[!0-9]*|"") die "AV_INSTANCE must be a non-negative integer (got '$AV_INSTANCE').";; esac
|
|
case "$AV_USE_DEBUG_ENCRYPTION_KEY" in true|false) ;; (*) die "AV_USE_DEBUG_ENCRYPTION_KEY must be 'true' or 'false' (got '$AV_USE_DEBUG_ENCRYPTION_KEY').";; esac
|
|
|
|
# --- Resolve effective ports ---------------------------------------------------
|
|
SERVICE_COUNT=10 # block width (offsets 0..9); DB sits last so it ends in 9
|
|
case "$AV_PORT_STRIDE" in (*[!0-9]*|"") die "AV_PORT_STRIDE must be a positive integer (got '$AV_PORT_STRIDE').";; esac
|
|
[ "$AV_PORT_STRIDE" -ge "$SERVICE_COUNT" ] || die "AV_PORT_STRIDE ($AV_PORT_STRIDE) must be >= $SERVICE_COUNT so instances don't overlap."
|
|
case "$AV_BASE_PORT" in (*[!0-9]*|"") die "AV_BASE_PORT must be a positive integer (got '$AV_BASE_PORT').";; esac
|
|
|
|
BLOCK_BASE=$(( AV_BASE_PORT + AV_INSTANCE * AV_PORT_STRIDE ))
|
|
# Dev only serves HTTP, so each web app reserves a single port.
|
|
API_HTTP=$(( BLOCK_BASE + 0 ))
|
|
CLIENT_HTTP=$(( BLOCK_BASE + 1 ))
|
|
ADMIN_HTTP=$(( BLOCK_BASE + 2 ))
|
|
EXT_PORT=$(( BLOCK_BASE + 3 ))
|
|
EXPO_PORT=$(( BLOCK_BASE + 4 ))
|
|
# DB sits at the last offset so its port always ends in 9 (e.g. 5109) — easy to
|
|
# remember. Offsets 5..8 are left free as spare slots inside the block.
|
|
DB_PORT=$(( BLOCK_BASE + 9 ))
|
|
|
|
# One dev DB container per instance, with the published port in the project
|
|
# name (e.g. 'aliasvault-dev-5109') so multiple instances can run side by side
|
|
# without start/stop/status checks hitting another instance's database.
|
|
# install.sh derives the same name from dev.env for its db-export/db-import.
|
|
DB_PROJECT="aliasvault-dev-$DB_PORT"
|
|
DB_COMPOSE="$ROOT_DIR/dockerfiles/docker-compose.dev.yml"
|
|
CONN_STR="Host=localhost;Port=$DB_PORT;Database=aliasvault;Username=aliasvault;Password=password;Maximum Pool Size=20;Minimum Pool Size=1"
|
|
|
|
# --- Helpers -------------------------------------------------------------------
|
|
RULE="────────────────────────────────────────────────────────────"
|
|
|
|
print_ports() {
|
|
printf "%sAliasVault dev — instance %s%s (base %s, stride %s → block %s..%s)\n" \
|
|
"$BOLD" "$AV_INSTANCE" "$NC" "$AV_BASE_PORT" "$AV_PORT_STRIDE" "$BLOCK_BASE" "$((BLOCK_BASE + SERVICE_COUNT - 1))"
|
|
printf " %-14s http://localhost:%s\n" "API" "$API_HTTP"
|
|
printf " %-14s http://localhost:%s\n" "Client" "$CLIENT_HTTP"
|
|
printf " %-14s http://localhost:%s\n" "Admin" "$ADMIN_HTTP"
|
|
printf " %-14s localhost:%s\n" "Postgres DB" "$DB_PORT"
|
|
printf " %-14s :%s\n" "Browser ext" "$EXT_PORT"
|
|
printf " %-14s :%s\n" "Mobile" "$EXPO_PORT"
|
|
}
|
|
|
|
# Print a clear, scannable header before a service takes over the terminal, so
|
|
# it's obvious at a glance which service it is and which ports it's on.
|
|
banner() {
|
|
local title="$1"; shift
|
|
printf "\n%s%s%s\n" "$CYAN" "$RULE" "$NC"
|
|
printf "%s ▶ %s%s %sinstance %s%s\n" "$BOLD" "$title" "$NC" "$DIM" "$AV_INSTANCE" "$NC"
|
|
local line
|
|
for line in "$@"; do
|
|
printf " %s\n" "$line"
|
|
done
|
|
printf "%s%s%s\n\n" "$CYAN" "$RULE" "$NC"
|
|
}
|
|
|
|
require() { command -v "$1" >/dev/null 2>&1 || die "'$1' is required but not installed."; }
|
|
|
|
# Generate the Blazor client's dev config.
|
|
write_client_dev_settings() {
|
|
local target="$ROOT_DIR/apps/server/AliasVault.Client/wwwroot/appsettings.Development.json"
|
|
cat > "$target" <<JSON
|
|
{
|
|
"ApiUrl": "http://localhost:$API_HTTP",
|
|
"PrivateEmailDomains": ["example.tld", "example2.tld", "disabled.tld"],
|
|
"HiddenPrivateEmailDomains": ["disabled.tld"],
|
|
"SupportEmail": "support@example.tld",
|
|
"PublicRegistrationEnabled": "true",
|
|
"DeploymentMode": "dev",
|
|
"UseDebugEncryptionKey": "$AV_USE_DEBUG_ENCRYPTION_KEY",
|
|
"CryptographyOverrideType": "Argon2Id",
|
|
"CryptographyOverrideSettings": "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
|
|
}
|
|
JSON
|
|
info "Wrote client dev config → ApiUrl http://localhost:$API_HTTP (UseDebugEncryptionKey=$AV_USE_DEBUG_ENCRYPTION_KEY)"
|
|
}
|
|
|
|
db_running() {
|
|
[ -n "$(AV_DB_PORT="$DB_PORT" docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" ps --status running -q 2>/dev/null)" ]
|
|
}
|
|
|
|
start_db() {
|
|
require docker
|
|
banner "Database — $DB_PROJECT" "Postgres localhost:$DB_PORT"
|
|
# --wait blocks until the container is healthy, so callers (and CI) can assume
|
|
# the database is ready to accept connections once this returns.
|
|
AV_DB_PORT="$DB_PORT" \
|
|
docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" up -d --build --wait --wait-timeout 60
|
|
info "Database ready: $CONN_STR"
|
|
}
|
|
|
|
stop_db() {
|
|
require docker
|
|
info "Stopping dev database '$DB_PROJECT' ..."
|
|
AV_DB_PORT="$DB_PORT" \
|
|
docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" down
|
|
}
|
|
|
|
# Toggle: stop the dev database if it's running, otherwise start it.
|
|
toggle_db() {
|
|
require docker
|
|
if db_running; then
|
|
stop_db
|
|
else
|
|
start_db
|
|
fi
|
|
}
|
|
|
|
# Run a .NET app with ports + DB driven by env/CLI flags.
|
|
# Binds to 0.0.0.0 so the service is reachable from other devices on the LAN
|
|
# (e.g. a phone running the mobile app), not just from localhost.
|
|
run_dotnet() {
|
|
local proj="$1" http="$2"
|
|
require dotnet
|
|
banner "$proj" \
|
|
"HTTP http://localhost:$http" \
|
|
"Database localhost:$DB_PORT"
|
|
ASPNETCORE_ENVIRONMENT="Development" \
|
|
ConnectionStrings__AliasServerDbContext="$CONN_STR" \
|
|
dotnet watch --project "$ROOT_DIR/apps/server/$proj" --no-launch-profile \
|
|
--urls "http://0.0.0.0:$http"
|
|
}
|
|
|
|
# Dev-only secrets. These are static fake values that are used only in dev.
|
|
DEV_JWT_KEY="${JWT_KEY:-12345678901234567890123456789012}"
|
|
DEV_DATA_PROTECTION_CERT_PASS="${DATA_PROTECTION_CERT_PASS:-Development}"
|
|
DEV_ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-AQAAAAIAAYagAAAAEKWfKfa2gh9Z72vjAlnNP1xlME7FsunRznzyrfqFte40FToufRwa3kX8wwDwnEXZag==}"
|
|
DEV_ADMIN_PASSWORD_GENERATED="${ADMIN_PASSWORD_GENERATED:-2030-01-01T00:00:00Z}"
|
|
DEV_PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-example.tld,example2.tld,aliasvault.net,disabled.tld}"
|
|
DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-disabled.tld}"
|
|
|
|
start_api() {
|
|
JWT_KEY="$DEV_JWT_KEY" \
|
|
DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \
|
|
PUBLIC_REGISTRATION_ENABLED="true" \
|
|
PRIVATE_EMAIL_DOMAINS="$DEV_PRIVATE_EMAIL_DOMAINS" \
|
|
HIDDEN_PRIVATE_EMAIL_DOMAINS="$DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS" \
|
|
IP_LOGGING_ENABLED="true" \
|
|
MAX_UPLOAD_SIZE_MB="100" \
|
|
run_dotnet "AliasVault.Api" "$API_HTTP"
|
|
}
|
|
start_admin() {
|
|
ADMIN_PASSWORD_HASH="$DEV_ADMIN_PASSWORD_HASH" \
|
|
ADMIN_PASSWORD_GENERATED="$DEV_ADMIN_PASSWORD_GENERATED" \
|
|
DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \
|
|
IP_LOGGING_ENABLED="true" \
|
|
run_dotnet "AliasVault.Admin" "$ADMIN_HTTP"
|
|
}
|
|
start_client() {
|
|
write_client_dev_settings
|
|
run_dotnet "AliasVault.Client" "$CLIENT_HTTP"
|
|
}
|
|
|
|
# Background workers (no HTTP port).
|
|
run_worker() {
|
|
local proj="$1"
|
|
require dotnet
|
|
banner "$proj (worker)" "Database localhost:$DB_PORT"
|
|
ConnectionStrings__AliasServerDbContext="$CONN_STR" \
|
|
dotnet watch --project "$ROOT_DIR/apps/server/$proj"
|
|
}
|
|
start_smtp() { run_worker "Services/AliasVault.SmtpService"; }
|
|
start_taskrunner() { run_worker "Services/AliasVault.TaskRunner"; }
|
|
|
|
start_ext() {
|
|
require npm
|
|
banner "Browser extension (Chrome dev)" \
|
|
"Dev server http://localhost:$EXT_PORT" \
|
|
"API http://localhost:$API_HTTP"
|
|
cd "$ROOT_DIR/apps/browser-extension"
|
|
npm run dev:chrome -- --port "$EXT_PORT"
|
|
}
|
|
|
|
start_mobile() {
|
|
require npx
|
|
banner "Mobile app (Expo)" \
|
|
"Metro http://localhost:$EXPO_PORT" \
|
|
"API http://localhost:$API_HTTP"
|
|
cd "$ROOT_DIR/apps/mobile-app"
|
|
EXPO_PUBLIC_API_URL="http://localhost:$API_HTTP" \
|
|
npx expo start --port "$EXPO_PORT"
|
|
}
|
|
|
|
# --- Interactive menu ----------------------------------------------------------
|
|
menu() {
|
|
print_ports
|
|
printf "\n%sWhich app do you want to start?%s\n" "$BOLD" "$NC"
|
|
# Quick live check so the DB entry shows the action it'll take + current state.
|
|
local db_label
|
|
if db_running; then
|
|
db_label="Stop dev DB ${GREEN}(running)${NC}"
|
|
else
|
|
db_label="Start dev DB ${DIM}(stopped)${NC}"
|
|
fi
|
|
local options=(
|
|
"api:API"
|
|
"client:Client"
|
|
"admin:Admin"
|
|
"ext:Browser extension"
|
|
"mobile:Mobile (Expo)"
|
|
"db:$db_label"
|
|
)
|
|
local i=1
|
|
for o in "${options[@]}"; do
|
|
printf " %s%2d%s) %s\n" "$CYAN" "$i" "$NC" "${o#*:}"; i=$((i+1))
|
|
done
|
|
printf "%sSelect [1-%d]:%s " "$BOLD" "${#options[@]}" "$NC"
|
|
local choice; read -r choice
|
|
case "$choice" in (*[!0-9]*|"") die "Invalid selection.";; esac
|
|
[ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ] || die "Selection out of range."
|
|
local picked="${options[$((choice-1))]}"
|
|
dispatch "${picked%%:*}"
|
|
}
|
|
|
|
# --- Dispatch ------------------------------------------------------------------
|
|
dispatch() {
|
|
case "${1:-}" in
|
|
api) start_api ;;
|
|
client) start_client ;;
|
|
admin) start_admin ;;
|
|
smtp) start_smtp ;;
|
|
taskrunner) start_taskrunner ;;
|
|
ext) start_ext ;;
|
|
mobile) start_mobile ;;
|
|
db) toggle_db ;;
|
|
db-start) start_db ;;
|
|
db-stop) stop_db ;;
|
|
ports) print_ports ;;
|
|
quit|q) exit 0 ;;
|
|
"") menu ;;
|
|
-h|--help|help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' ;;
|
|
*) die "Unknown command '$1'.
|
|
start: api/client/admin/smtp/taskrunner/ext/mobile
|
|
db: db (toggle) / db-start / db-stop" ;;
|
|
esac
|
|
}
|
|
|
|
dispatch "${1:-}"
|