mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Initial release — Sprout Nostr relay with enterprise extensions (#2)
This commit is contained in:
54
scripts/dev-reset.sh
Executable file
54
scripts/dev-reset.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dev-reset.sh — Tear down everything and recreate a clean environment
|
||||
# =============================================================================
|
||||
# Usage: ./scripts/dev-reset.sh
|
||||
#
|
||||
# Stops all services, removes ALL volumes (data is lost!), brings everything
|
||||
# back up fresh, and runs migrations.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[dev-reset]${NC} $*"; }
|
||||
success(){ echo -e "${GREEN}[dev-reset]${NC} ✅ $*"; }
|
||||
warn() { echo -e "${YELLOW}[dev-reset]${NC} ⚠️ $*"; }
|
||||
error() { echo -e "${RED}[dev-reset]${NC} ❌ $*" >&2; }
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# ---- Confirm ----------------------------------------------------------------
|
||||
|
||||
if [[ "${1:-}" != "--yes" ]]; then
|
||||
echo -e "${YELLOW}⚠️ WARNING: This will DELETE all local data (mysql, typesense volumes).${NC}"
|
||||
echo -e " Redis data is ephemeral and always wiped on restart."
|
||||
echo ""
|
||||
read -r -p "Are you sure? [y/N] " confirm
|
||||
case "${confirm}" in
|
||||
[yY][eE][sS]|[yY]) ;;
|
||||
*)
|
||||
log "Aborted."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ---- Tear down --------------------------------------------------------------
|
||||
|
||||
log "Stopping and removing containers + volumes..."
|
||||
docker compose down -v --remove-orphans 2>/dev/null || true
|
||||
success "Containers and volumes removed"
|
||||
|
||||
# ---- Bring back up ----------------------------------------------------------
|
||||
|
||||
log "Recreating environment..."
|
||||
exec "${SCRIPT_DIR}/dev-setup.sh"
|
||||
155
scripts/dev-setup.sh
Executable file
155
scripts/dev-setup.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dev-setup.sh — One-shot local dev environment setup
|
||||
# =============================================================================
|
||||
# Usage: ./scripts/dev-setup.sh
|
||||
#
|
||||
# Starts all Docker services, waits for healthy, runs migrations, prints
|
||||
# connection info and next steps.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
TIMEOUT=120 # seconds to wait for services to become healthy
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log() { echo -e "${BLUE}[dev-setup]${NC} $*"; }
|
||||
success(){ echo -e "${GREEN}[dev-setup]${NC} ✅ $*"; }
|
||||
warn() { echo -e "${YELLOW}[dev-setup]${NC} ⚠️ $*"; }
|
||||
error() { echo -e "${RED}[dev-setup]${NC} ❌ $*" >&2; }
|
||||
|
||||
# ---- Preflight checks -------------------------------------------------------
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
error "Docker not found. Install Docker Desktop: https://www.docker.com/products/docker-desktop/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
error "Docker daemon is not running. Start Docker Desktop and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# ---- Start services ---------------------------------------------------------
|
||||
|
||||
log "Starting services..."
|
||||
docker compose up -d
|
||||
|
||||
# ---- Wait for healthy -------------------------------------------------------
|
||||
|
||||
wait_healthy() {
|
||||
local service="$1"
|
||||
local container="$2"
|
||||
local elapsed=0
|
||||
local interval=3
|
||||
|
||||
log "Waiting for ${service} to be healthy..."
|
||||
while true; do
|
||||
local status
|
||||
status=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "not_found")
|
||||
|
||||
case "${status}" in
|
||||
healthy)
|
||||
success "${service} is healthy"
|
||||
return 0
|
||||
;;
|
||||
unhealthy)
|
||||
error "${service} is unhealthy. Check logs: docker logs ${container}"
|
||||
return 1
|
||||
;;
|
||||
not_found)
|
||||
error "Container ${container} not found"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
|
||||
error "Timed out waiting for ${service} (${TIMEOUT}s). Check: docker logs ${container}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep "${interval}"
|
||||
elapsed=$((elapsed + interval))
|
||||
echo -n "."
|
||||
done
|
||||
}
|
||||
|
||||
echo ""
|
||||
wait_healthy "MySQL" "sprout-mysql"
|
||||
wait_healthy "Redis" "sprout-redis"
|
||||
wait_healthy "Typesense" "sprout-typesense"
|
||||
echo ""
|
||||
|
||||
# ---- Run migrations ---------------------------------------------------------
|
||||
|
||||
log "Running database migrations..."
|
||||
|
||||
MIGRATION_DIR="${REPO_ROOT}/migrations"
|
||||
|
||||
if [[ ! -d "${MIGRATION_DIR}" ]]; then
|
||||
warn "No migrations directory found at ${MIGRATION_DIR}. Skipping."
|
||||
else
|
||||
# Check if sqlx CLI is available (preferred)
|
||||
if command -v sqlx &>/dev/null; then
|
||||
log "Using sqlx CLI for migrations..."
|
||||
DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout" \
|
||||
sqlx migrate run --source "${MIGRATION_DIR}"
|
||||
success "Migrations applied via sqlx"
|
||||
else
|
||||
# Fallback: run SQL files directly via mysql in the container
|
||||
log "sqlx CLI not found — applying migrations via mysql CLI..."
|
||||
shopt -s nullglob
|
||||
SQL_FILES=("${MIGRATION_DIR}"/*.sql)
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#SQL_FILES[@]} -eq 0 ]]; then
|
||||
warn "No .sql files found in ${MIGRATION_DIR}. Skipping."
|
||||
else
|
||||
for sql_file in "${SQL_FILES[@]}"; do
|
||||
filename="$(basename "${sql_file}")"
|
||||
log " Applying ${filename}..."
|
||||
docker exec -i sprout-mysql \
|
||||
mysql -u sprout -psprout_dev sprout \
|
||||
< "${sql_file}"
|
||||
done
|
||||
success "Migrations applied via mysql"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- Print connection info --------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Sprout dev environment is ready! 🌱${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BLUE}MySQL${NC} mysql://sprout:sprout_dev@localhost:3306/sprout"
|
||||
echo -e " ${BLUE}Redis${NC} redis://localhost:6379"
|
||||
echo -e " ${BLUE}Typesense${NC} http://localhost:8108 (key: sprout_dev_key)"
|
||||
echo -e " ${BLUE}Adminer${NC} http://localhost:8082 (DB browser)"
|
||||
echo -e " ${BLUE}Keycloak${NC} http://localhost:8180 (admin / admin — local OAuth testing)"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Next steps:${NC}"
|
||||
echo -e " cp .env.example .env # configure your environment"
|
||||
echo -e " bash scripts/setup-keycloak.sh # configure Keycloak for OAuth testing (optional)"
|
||||
echo -e " cargo run -p sprout-relay # start the relay server"
|
||||
echo -e " ./scripts/run-tests.sh # run all tests"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Useful commands:${NC}"
|
||||
echo -e " docker compose ps # check service status"
|
||||
echo -e " docker compose logs -f # tail all logs"
|
||||
echo -e " docker compose down # stop services (keep data)"
|
||||
echo -e " ./scripts/dev-reset.sh # wipe and start fresh"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
208
scripts/run-tests.sh
Executable file
208
scripts/run-tests.sh
Executable file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# run-tests.sh — Run Sprout test suite
|
||||
# =============================================================================
|
||||
# Usage:
|
||||
# ./scripts/run-tests.sh # run all tests (default)
|
||||
# ./scripts/run-tests.sh unit # unit tests only (no infra needed)
|
||||
# ./scripts/run-tests.sh integration # integration tests only
|
||||
# ./scripts/run-tests.sh all # explicit all
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
MODE="${1:-all}"
|
||||
TIMEOUT=60 # seconds to wait for services if starting them
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[run-tests]${NC} $*"; }
|
||||
success(){ echo -e "${GREEN}[run-tests]${NC} ✅ $*"; }
|
||||
warn() { echo -e "${YELLOW}[run-tests]${NC} ⚠️ $*"; }
|
||||
error() { echo -e "${RED}[run-tests]${NC} ❌ $*" >&2; }
|
||||
section(){ echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; }
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
# ---- Load .env if present ---------------------------------------------------
|
||||
|
||||
if [[ -f ".env" ]]; then
|
||||
log "Loading .env..."
|
||||
set -o allexport
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +o allexport
|
||||
else
|
||||
# Use defaults matching docker-compose.yml
|
||||
export DATABASE_URL="mysql://sprout:sprout_dev@localhost:3306/sprout"
|
||||
export REDIS_URL="redis://localhost:6379"
|
||||
export TYPESENSE_API_KEY="sprout_dev_key"
|
||||
export TYPESENSE_URL="http://localhost:8108"
|
||||
fi
|
||||
|
||||
# ---- Track results ----------------------------------------------------------
|
||||
|
||||
declare -a PASSED=()
|
||||
declare -a FAILED=()
|
||||
|
||||
run_test_step() {
|
||||
local name="$1"
|
||||
shift
|
||||
log "Running: ${name}"
|
||||
if "$@"; then
|
||||
success "${name} passed"
|
||||
PASSED+=("${name}")
|
||||
else
|
||||
error "${name} FAILED"
|
||||
FAILED+=("${name}")
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Check / start infra (for integration tests) ----------------------------
|
||||
|
||||
services_healthy() {
|
||||
local mysql_ok redis_ok
|
||||
mysql_ok=$(docker inspect --format='{{.State.Health.Status}}' sprout-mysql 2>/dev/null || echo "not_found")
|
||||
redis_ok=$(docker inspect --format='{{.State.Health.Status}}' sprout-redis 2>/dev/null || echo "not_found")
|
||||
[[ "${mysql_ok}" == "healthy" && "${redis_ok}" == "healthy" ]]
|
||||
}
|
||||
|
||||
ensure_services() {
|
||||
if services_healthy; then
|
||||
success "Services already healthy"
|
||||
return 0
|
||||
fi
|
||||
|
||||
warn "Services not running — starting them..."
|
||||
docker compose up -d
|
||||
|
||||
local elapsed=0
|
||||
local interval=3
|
||||
while ! services_healthy; do
|
||||
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
|
||||
error "Timed out waiting for services (${TIMEOUT}s)"
|
||||
return 1
|
||||
fi
|
||||
sleep "${interval}"
|
||||
elapsed=$((elapsed + interval))
|
||||
echo -n "."
|
||||
done
|
||||
echo ""
|
||||
success "Services healthy"
|
||||
|
||||
# Ensure migrations are current
|
||||
ensure_migrations
|
||||
}
|
||||
|
||||
ensure_migrations() {
|
||||
log "Ensuring migrations are current..."
|
||||
local migration_dir="${REPO_ROOT}/migrations"
|
||||
|
||||
if [[ ! -d "${migration_dir}" ]]; then
|
||||
warn "No migrations directory. Skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v sqlx &>/dev/null; then
|
||||
DATABASE_URL="${DATABASE_URL}" sqlx migrate run --source "${migration_dir}" 2>/dev/null \
|
||||
&& success "Migrations current" \
|
||||
|| warn "sqlx migrate run failed — DB may be out of date"
|
||||
else
|
||||
# Fallback: apply all SQL files (idempotent if schema uses IF NOT EXISTS)
|
||||
shopt -s nullglob
|
||||
local sql_files=("${migration_dir}"/*.sql)
|
||||
shopt -u nullglob
|
||||
for sql_file in "${sql_files[@]}"; do
|
||||
docker exec -i sprout-mysql \
|
||||
mysql -u sprout -psprout_dev sprout \
|
||||
< "${sql_file}" &>/dev/null || true
|
||||
done
|
||||
success "Migrations applied (mysql fallback)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- Unit tests (no infra needed) -------------------------------------------
|
||||
|
||||
run_unit_tests() {
|
||||
section "Unit Tests (no infra required)"
|
||||
|
||||
run_test_step "sprout-core tests" \
|
||||
cargo test -p sprout-core --lib -- --nocapture
|
||||
|
||||
run_test_step "sprout-auth unit tests" \
|
||||
cargo test -p sprout-auth --lib -- --nocapture
|
||||
}
|
||||
|
||||
# ---- DB / integration tests (infra required) --------------------------------
|
||||
|
||||
run_integration_tests() {
|
||||
section "Integration Tests (requires running services)"
|
||||
|
||||
ensure_services
|
||||
|
||||
run_test_step "sprout-db tests" \
|
||||
cargo test -p sprout-db -- --nocapture
|
||||
|
||||
run_test_step "sprout-auth integration tests" \
|
||||
cargo test -p sprout-auth --test '*' -- --nocapture 2>/dev/null || \
|
||||
run_test_step "sprout-auth (no integration tests found)" true
|
||||
|
||||
run_test_step "workspace integration tests" \
|
||||
cargo test --test '*' -- --nocapture 2>/dev/null || \
|
||||
run_test_step "workspace integration tests (none found)" true
|
||||
}
|
||||
|
||||
# ---- Main -------------------------------------------------------------------
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
case "${MODE}" in
|
||||
unit)
|
||||
run_unit_tests
|
||||
;;
|
||||
integration)
|
||||
run_integration_tests
|
||||
;;
|
||||
all|*)
|
||||
run_unit_tests
|
||||
run_integration_tests
|
||||
;;
|
||||
esac
|
||||
|
||||
END_TIME=$(date +%s)
|
||||
ELAPSED=$((END_TIME - START_TIME))
|
||||
|
||||
# ---- Summary ----------------------------------------------------------------
|
||||
|
||||
section "Test Summary"
|
||||
echo ""
|
||||
echo -e " Duration: ${ELAPSED}s"
|
||||
echo ""
|
||||
|
||||
if [[ ${#PASSED[@]} -gt 0 ]]; then
|
||||
echo -e " ${GREEN}Passed (${#PASSED[@]}):${NC}"
|
||||
for t in "${PASSED[@]}"; do
|
||||
echo -e " ${GREEN}✅${NC} ${t}"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo -e " ${RED}Failed (${#FAILED[@]}):${NC}"
|
||||
for t in "${FAILED[@]}"; do
|
||||
echo -e " ${RED}❌${NC} ${t}"
|
||||
done
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
success "All tests passed! 🎉"
|
||||
exit 0
|
||||
273
scripts/setup-keycloak.sh
Executable file
273
scripts/setup-keycloak.sh
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# setup-keycloak.sh — Configure Keycloak for local OAuth testing
|
||||
# =============================================================================
|
||||
# Usage: ./scripts/setup-keycloak.sh
|
||||
#
|
||||
# Creates the `sprout` realm with:
|
||||
# - sprout-desktop client (public, direct access grants)
|
||||
# - Test users: tyler, alice, bob, charlie (password: password123)
|
||||
# - nostr_pubkey custom attribute on each user
|
||||
# - Protocol mapper: nostr_pubkey → JWT access token claim
|
||||
#
|
||||
# Keycloak is a LOCAL DEV STAND-IN for Okta/generic OIDC providers.
|
||||
# It is NOT a production dependency.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Keycloak running at http://localhost:8180 (docker compose up -d)
|
||||
# - curl and jq installed
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8180}"
|
||||
ADMIN_USER="${KEYCLOAK_ADMIN:-admin}"
|
||||
ADMIN_PASS="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
||||
REALM="sprout"
|
||||
CLIENT_ID="sprout-desktop"
|
||||
TIMEOUT=120 # seconds to wait for Keycloak
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log() { echo -e "${BLUE}[keycloak-setup]${NC} $*"; }
|
||||
success(){ echo -e "${GREEN}[keycloak-setup]${NC} ✅ $*"; }
|
||||
warn() { echo -e "${YELLOW}[keycloak-setup]${NC} ⚠️ $*"; }
|
||||
error() { echo -e "${RED}[keycloak-setup]${NC} ❌ $*" >&2; }
|
||||
|
||||
# ---- Preflight --------------------------------------------------------------
|
||||
|
||||
for cmd in curl jq; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
error "Required tool not found: $cmd"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- Wait for Keycloak ------------------------------------------------------
|
||||
|
||||
log "Waiting for Keycloak at ${KEYCLOAK_URL}..."
|
||||
elapsed=0
|
||||
interval=5
|
||||
until curl -sf "${KEYCLOAK_URL}/health/ready" -o /dev/null 2>/dev/null; do
|
||||
if [[ ${elapsed} -ge ${TIMEOUT} ]]; then
|
||||
error "Timed out waiting for Keycloak (${TIMEOUT}s). Is it running?"
|
||||
error " docker compose up -d keycloak"
|
||||
exit 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep "${interval}"
|
||||
elapsed=$((elapsed + interval))
|
||||
done
|
||||
echo ""
|
||||
success "Keycloak is ready"
|
||||
|
||||
# ---- Get admin token --------------------------------------------------------
|
||||
|
||||
log "Authenticating as admin..."
|
||||
ADMIN_TOKEN=$(curl -sf \
|
||||
-X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "client_id=admin-cli" \
|
||||
-d "username=${ADMIN_USER}" \
|
||||
-d "password=${ADMIN_PASS}" \
|
||||
-d "grant_type=password" \
|
||||
| jq -r '.access_token')
|
||||
|
||||
if [[ -z "${ADMIN_TOKEN}" || "${ADMIN_TOKEN}" == "null" ]]; then
|
||||
error "Failed to get admin token. Check KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
success "Admin token obtained"
|
||||
|
||||
# Helper: authenticated API call
|
||||
kc() {
|
||||
local method="$1"; shift
|
||||
local path="$1"; shift
|
||||
curl -sf \
|
||||
-X "${method}" \
|
||||
"${KEYCLOAK_URL}/admin/realms${path}" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
kc_root() {
|
||||
local method="$1"; shift
|
||||
local path="$1"; shift
|
||||
curl -sf \
|
||||
-X "${method}" \
|
||||
"${KEYCLOAK_URL}/admin${path}" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
# ---- Create realm -----------------------------------------------------------
|
||||
|
||||
log "Checking for realm '${REALM}'..."
|
||||
REALM_EXISTS=$(curl -sf \
|
||||
"${KEYCLOAK_URL}/admin/realms/${REALM}" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
|
||||
|
||||
if [[ "${REALM_EXISTS}" == "200" ]]; then
|
||||
warn "Realm '${REALM}' already exists — skipping creation"
|
||||
else
|
||||
log "Creating realm '${REALM}'..."
|
||||
curl -sf \
|
||||
-X POST "${KEYCLOAK_URL}/admin/realms" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"realm": "'"${REALM}"'",
|
||||
"displayName": "Sprout",
|
||||
"enabled": true,
|
||||
"registrationAllowed": false,
|
||||
"loginWithEmailAllowed": true,
|
||||
"duplicateEmailsAllowed": false,
|
||||
"resetPasswordAllowed": false,
|
||||
"editUsernameAllowed": false,
|
||||
"bruteForceProtected": false
|
||||
}'
|
||||
success "Realm '${REALM}' created"
|
||||
fi
|
||||
|
||||
# ---- Create client ----------------------------------------------------------
|
||||
|
||||
log "Checking for client '${CLIENT_ID}'..."
|
||||
EXISTING_CLIENT=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "${EXISTING_CLIENT}" ]]; then
|
||||
warn "Client '${CLIENT_ID}' already exists (id: ${EXISTING_CLIENT}) — skipping creation"
|
||||
CLIENT_UUID="${EXISTING_CLIENT}"
|
||||
else
|
||||
log "Creating client '${CLIENT_ID}'..."
|
||||
kc POST "/${REALM}/clients" -d '{
|
||||
"clientId": "'"${CLIENT_ID}"'",
|
||||
"name": "Sprout Desktop",
|
||||
"enabled": true,
|
||||
"publicClient": true,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"serviceAccountsEnabled": false,
|
||||
"redirectUris": [
|
||||
"http://localhost:*",
|
||||
"sprout://*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"protocol": "openid-connect"
|
||||
}'
|
||||
|
||||
CLIENT_UUID=$(kc GET "/${REALM}/clients?clientId=${CLIENT_ID}" | jq -r '.[0].id')
|
||||
success "Client '${CLIENT_ID}' created (id: ${CLIENT_UUID})"
|
||||
fi
|
||||
|
||||
# ---- Add nostr_pubkey protocol mapper ---------------------------------------
|
||||
|
||||
log "Checking for nostr_pubkey protocol mapper..."
|
||||
MAPPER_EXISTS=$(kc GET "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" \
|
||||
| jq -r '.[] | select(.name == "nostr_pubkey") | .id // empty')
|
||||
|
||||
if [[ -n "${MAPPER_EXISTS}" ]]; then
|
||||
warn "Protocol mapper 'nostr_pubkey' already exists — skipping"
|
||||
else
|
||||
log "Creating nostr_pubkey → JWT claim mapper..."
|
||||
kc POST "/${REALM}/clients/${CLIENT_UUID}/protocol-mappers/models" -d '{
|
||||
"name": "nostr_pubkey",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"user.attribute": "nostr_pubkey",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "nostr_pubkey",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
}'
|
||||
success "Protocol mapper 'nostr_pubkey' created"
|
||||
fi
|
||||
|
||||
# ---- Create users -----------------------------------------------------------
|
||||
|
||||
# Format: "username:nostr_pubkey"
|
||||
declare -a USERS=(
|
||||
"tyler:e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
|
||||
"alice:953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
|
||||
"bob:bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
|
||||
"charlie:554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
|
||||
)
|
||||
|
||||
for entry in "${USERS[@]}"; do
|
||||
username="${entry%%:*}"
|
||||
pubkey="${entry##*:}"
|
||||
|
||||
log "Checking for user '${username}'..."
|
||||
EXISTING_USER=$(kc GET "/${REALM}/users?username=${username}&exact=true" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "${EXISTING_USER}" ]]; then
|
||||
warn "User '${username}' already exists (id: ${EXISTING_USER}) — updating nostr_pubkey attribute"
|
||||
kc PUT "/${REALM}/users/${EXISTING_USER}" -d '{
|
||||
"attributes": {
|
||||
"nostr_pubkey": ["'"${pubkey}"'"]
|
||||
}
|
||||
}'
|
||||
success "User '${username}' updated"
|
||||
else
|
||||
log "Creating user '${username}'..."
|
||||
kc POST "/${REALM}/users" -d '{
|
||||
"username": "'"${username}"'",
|
||||
"email": "'"${username}"'@sprout.local",
|
||||
"firstName": "'"${username^}"'",
|
||||
"lastName": "Test",
|
||||
"enabled": true,
|
||||
"emailVerified": true,
|
||||
"credentials": [{
|
||||
"type": "password",
|
||||
"value": "password123",
|
||||
"temporary": false
|
||||
}],
|
||||
"attributes": {
|
||||
"nostr_pubkey": ["'"${pubkey}"'"]
|
||||
}
|
||||
}'
|
||||
success "User '${username}' created (nostr_pubkey: ${pubkey:0:16}...)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- Summary ----------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Keycloak realm setup complete! 🔑${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Admin UI${NC} http://localhost:8180 (admin / admin)"
|
||||
echo -e " ${BLUE}Realm${NC} ${REALM}"
|
||||
echo -e " ${BLUE}Client${NC} ${CLIENT_ID} (public, direct access grants)"
|
||||
echo ""
|
||||
echo -e " ${BLUE}Test users${NC} (password: password123)"
|
||||
echo -e " tyler e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"
|
||||
echo -e " alice 953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"
|
||||
echo -e " bob bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"
|
||||
echo -e " charlie 554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Relay env vars for Keycloak:${NC}"
|
||||
echo -e " OKTA_JWKS_URI=http://localhost:8180/realms/sprout/protocol/openid-connect/certs"
|
||||
echo -e " OKTA_ISSUER=http://localhost:8180/realms/sprout"
|
||||
echo -e " OKTA_AUDIENCE=sprout-desktop"
|
||||
echo -e " OKTA_PUBKEY_CLAIM=nostr_pubkey"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Get a token (direct grant):${NC}"
|
||||
echo -e " curl -s -X POST http://localhost:8180/realms/sprout/protocol/openid-connect/token \\"
|
||||
echo -e " -d 'client_id=sprout-desktop&grant_type=password&username=tyler&password=password123' \\"
|
||||
echo -e " | jq -r .access_token"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user