Files
deepdrft/deploy/setup-step10-creds.sh
T
daniel-c-harvey 564b704803 fix(installer): prompt for and write AuthBlocks:Email:From
Without this field, DeepDrftAPI throws InvalidOperationException on
startup. Adds the EMAIL_FROM prompt after EMAIL_TOKEN, writes "From"
into the Email JSON object, and unsets the variable on cleanup.
2026-06-23 05:26:48 -04:00

249 lines
10 KiB
Bash

#!/usr/bin/env bash
# deploy/setup-step10-creds.sh
# Run as the app user on the host. Implements Step 6 of install.sh.
# Prompts interactively for every secret, writes JSON credential files
# directly to ${APP_HOME}/.config/credentials/.
#
# Called by install.sh (which exports APP_USER, APP_HOME, PG_ROLE, DB_META,
# DB_AUTH, DOMAIN_PUBLIC, DOMAIN_APP before invoking). Can also be run
# standalone — falls back to defaults so the script remains independently useful.
#
# Idempotency: safe to re-run. By default, credentials that already exist in
# ${CREDDIR} are left alone (no re-prompting). Pass --force as the first arg to
# re-prompt for every secret and overwrite all credential files.
#
# Credential files and their consumers:
# filedatabase.json -> DeepDrftAPI (LoadCredential id: filedatabase)
# apikey.json -> DeepDrftAPI (LoadCredential id: apikey)
# connections.json -> DeepDrftAPI (LoadCredential id: connections)
# authblocks.json -> DeepDrftAPI (LoadCredential id: authblocks)
# api-public.json -> DeepDrftPublic (LoadCredential id: api)
# api-manager.json -> DeepDrftManager (LoadCredential id: api)
#
# The LoadCredential ids (left of colon in the unit file) must exactly match
# CredentialTools.ResolvePathOrThrow keys in the application code.
set -euo pipefail
# Escape a string for safe use as inline JSON value.
# Escapes backslash then double-quote (the two characters that break JSON string literals).
# Does not handle embedded newlines or control chars — avoid those in secrets.
json_escape() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
# Accept vars from environment (exported by install.sh) or fall back to defaults.
APP_USER="${APP_USER:-deepdrft}"
export XDG_RUNTIME_DIR="/run/user/$(id -u "${APP_USER}" 2>/dev/null || id -u)"
APP_HOME="${APP_HOME:-/${APP_USER}}"
PG_ROLE="${PG_ROLE:-deepdrft}"
DB_META="${DB_META:-deepdrft-meta}"
DB_AUTH="${DB_AUTH:-deepdrft-auth}"
DOMAIN_PUBLIC="${DOMAIN_PUBLIC:-deepdrft.com}"
DOMAIN_APP="${DOMAIN_APP:-app.${DOMAIN_PUBLIC}}"
CREDDIR="${APP_HOME}/.config/credentials"
EXPECTED_COUNT=6
# ── Parse args ────────────────────────────────────────────────────────────────
FORCE=0
if [[ "${1:-}" == "--force" ]]; then
FORCE=1
echo "[setup-step10-creds] --force: will re-prompt and overwrite all credentials"
fi
# ── Credential directory ──────────────────────────────────────────────────────
if [[ ! -d "${CREDDIR}" ]]; then
mkdir -p "${CREDDIR}"
chmod 700 "${CREDDIR}"
fi
echo "[setup-step10-creds] ${CREDDIR} ready"
# ── Helpers ───────────────────────────────────────────────────────────────────
# write_cred <name> <content>
# Writes JSON content to ${CREDDIR}/<name>.json (mode 600, owner APP_USER).
write_cred() {
local name="$1"
local content="$2"
local tmp
tmp="$(mktemp /tmp/deepdrft-cred-XXXXXXXX.json)"
printf '%s\n' "${content}" > "${tmp}"
install -m 600 -o "${APP_USER}" -g "${APP_USER}" "${tmp}" "${CREDDIR}/${name}.json"
rm -f "${tmp}"
echo "[setup-step10-creds] wrote ${name}.json"
}
# need_cred <name>
# Returns 0 (true) when the named credential should be (re)written:
# - --force was passed, OR
# - ${CREDDIR}/<name>.json does not yet exist.
need_cred() {
local name="$1"
if [[ "${FORCE}" -eq 1 ]]; then
return 0
fi
if [[ ! -f "${CREDDIR}/${name}.json" ]]; then
return 0
fi
return 1
}
# ── 1. filedatabase.json — no prompts, path fully templated ──────────────────
if need_cred "filedatabase"; then
write_cred "filedatabase" \
"{\"FileDatabaseSettings\":{\"VaultPath\":\"${APP_HOME}/api/deepdrft/vaults\"}}"
else
echo "[setup-step10-creds] filedatabase.json already exists, skipping"
fi
# ── 2. apikey.json — prompt or generate; value reused in api-manager.json ────
API_KEY=""
if need_cred "apikey"; then
echo
read -rp " API key (leave blank to generate with openssl rand -hex 32): " API_KEY_INPUT
if [[ -z "${API_KEY_INPUT}" ]]; then
API_KEY="$(openssl rand -hex 32)"
echo " [generated] API key: ${API_KEY}"
else
API_KEY="${API_KEY_INPUT}"
fi
unset API_KEY_INPUT
write_cred "apikey" \
"{\"ApiKeySettings\":{\"ApiKey\":\"$(json_escape "${API_KEY}")\"}}"
else
echo "[setup-step10-creds] apikey.json already exists, skipping"
# Still need the value for api-manager.json if that's also being written.
# Read it back from the existing file if jq is available, otherwise prompt again.
if need_cred "api-manager"; then
if command -v jq &>/dev/null; then
API_KEY="$(jq -r '.ApiKeySettings.ApiKey' "${CREDDIR}/apikey.json")"
echo "[setup-step10-creds] API key read from existing apikey.json"
else
echo
echo " apikey.json exists but api-manager.json is missing."
read -rp " Re-enter the API key (needed for api-manager.json): " API_KEY
fi
fi
fi
# ── 3. connections.json — prompt for PG password; DB names from env vars ─────
if need_cred "connections"; then
echo
echo " PostgreSQL connection strings for DeepDrftAPI."
echo " DB names: meta='${DB_META}', auth='${DB_AUTH}', role='${PG_ROLE}'"
echo
read -rsp " PostgreSQL password for the '${PG_ROLE}' role: " PG_PASSWORD
echo
read -rsp " Confirm password: " PG_PASSWORD_CONFIRM
echo
if [[ "${PG_PASSWORD}" != "${PG_PASSWORD_CONFIRM}" ]]; then
echo "[setup-step10-creds] ERROR: passwords do not match" >&2
exit 1
fi
if [[ -z "${PG_PASSWORD}" ]]; then
echo "[setup-step10-creds] ERROR: password cannot be empty" >&2
exit 1
fi
unset PG_PASSWORD_CONFIRM
META_CONN="Host=localhost;Database=${DB_META};Username=${PG_ROLE};Password=$(json_escape "${PG_PASSWORD}")"
AUTH_CONN="Host=localhost;Database=${DB_AUTH};Username=${PG_ROLE};Password=$(json_escape "${PG_PASSWORD}")"
write_cred "connections" \
"{\"ConnectionStrings\":{\"DefaultConnection\":\"${META_CONN}\",\"Auth\":\"${AUTH_CONN}\"}}"
unset PG_PASSWORD META_CONN AUTH_CONN
else
echo "[setup-step10-creds] connections.json already exists, skipping"
fi
# ── 4. authblocks.json — prompt for all AuthBlocks secrets ───────────────────
if need_cred "authblocks"; then
echo
echo " AuthBlocks configuration (JWT, email, admin account)."
echo
# JWT secret
read -rp " JWT secret (leave blank to generate): " JWT_SECRET_INPUT
if [[ -z "${JWT_SECRET_INPUT}" ]]; then
JWT_SECRET="$(openssl rand -hex 32)"
echo " [generated] JWT secret"
else
JWT_SECRET="${JWT_SECRET_INPUT}"
fi
unset JWT_SECRET_INPUT
# JWT issuer / audience
read -rp " JWT issuer (e.g. https://${DOMAIN_PUBLIC}): " JWT_ISSUER
read -rp " JWT audience (e.g. https://${DOMAIN_PUBLIC}): " JWT_AUDIENCE
# Email
echo
echo " Email provider (SMTP/API — used by AuthBlocks for verification emails)."
read -rp " Email host (SMTP server or API host): " EMAIL_HOST
read -rsp " Email token (API key / SMTP password): " EMAIL_TOKEN
echo
read -rp " Sender email address (From:, e.g. noreply@${DOMAIN_PUBLIC}): " EMAIL_FROM
# Admin account
echo
echo " Initial admin account for DeepDrft."
read -rp " Admin username: " ADMIN_USERNAME
read -rp " Admin email: " ADMIN_EMAIL
read -rsp " Admin password: " ADMIN_PASSWORD
echo
read -rsp " Confirm admin password: " ADMIN_PASSWORD_CONFIRM
echo
if [[ "${ADMIN_PASSWORD}" != "${ADMIN_PASSWORD_CONFIRM}" ]]; then
echo "[setup-step10-creds] ERROR: admin passwords do not match" >&2
exit 1
fi
unset ADMIN_PASSWORD_CONFIRM
# Support email
read -rp " Support email address: " SUPPORT_EMAIL
write_cred "authblocks" "$(cat <<JSON
{"AuthBlocks":{"Jwt":{"Secret":"$(json_escape "${JWT_SECRET}")","Issuer":"$(json_escape "${JWT_ISSUER}")","Audience":"$(json_escape "${JWT_AUDIENCE}")"},"Email":{"Host":"$(json_escape "${EMAIL_HOST}")","Token":"$(json_escape "${EMAIL_TOKEN}")","From":"$(json_escape "${EMAIL_FROM}")"},"Admin":{"UserName":"$(json_escape "${ADMIN_USERNAME}")","Email":"$(json_escape "${ADMIN_EMAIL}")","Password":"$(json_escape "${ADMIN_PASSWORD}")"},"SupportEmail":"$(json_escape "${SUPPORT_EMAIL}")"}}
JSON
)"
unset JWT_SECRET JWT_ISSUER JWT_AUDIENCE EMAIL_HOST EMAIL_TOKEN EMAIL_FROM
unset ADMIN_USERNAME ADMIN_EMAIL ADMIN_PASSWORD SUPPORT_EMAIL
else
echo "[setup-step10-creds] authblocks.json already exists, skipping"
fi
# ── 5. api-public.json — no prompts, static localhost URL ────────────────────
if need_cred "api-public"; then
write_cred "api-public" \
"{\"Api\":{\"ContentApiUrl\":\"http://localhost:${PORT_API:-5002}\"}}"
else
echo "[setup-step10-creds] api-public.json already exists, skipping"
fi
# ── 6. api-manager.json — reuses API key from step 2 ─────────────────────────
if need_cred "api-manager"; then
if [[ -z "${API_KEY}" ]]; then
echo
echo " api-manager.json needs the same API key as apikey.json."
read -rp " Enter the API key: " API_KEY
fi
write_cred "api-manager" \
"{\"Api\":{\"ContentApiUrl\":\"http://localhost:${PORT_API:-5002}\",\"ContentApiKey\":\"$(json_escape "${API_KEY}")\"}}"
unset API_KEY
else
echo "[setup-step10-creds] api-manager.json already exists, skipping"
unset API_KEY 2>/dev/null || true
fi
# ── Verify ────────────────────────────────────────────────────────────────────
echo ""
echo "[setup-step10-creds] verifying ${CREDDIR}:"
ls -la "${CREDDIR}/"
ACTUAL_COUNT="$(ls "${CREDDIR}/"*.json 2>/dev/null | wc -l)"
if [[ "${ACTUAL_COUNT}" -ne "${EXPECTED_COUNT}" ]]; then
echo "[setup-step10-creds] ERROR: expected ${EXPECTED_COUNT} .json files, found ${ACTUAL_COUNT}" >&2
exit 1
fi
echo "[setup-step10-creds] all ${EXPECTED_COUNT} credentials present — step 6 complete"