#!/usr/bin/env bash # # credentials.sh — Shared credential loader for Mosaic tool suites # # Usage: source ~/.config/mosaic/tools/_lib/credentials.sh # load_credentials # # Loads credentials from environment variables first, then falls back # to ~/src/jarvis-brain/credentials.json (or MOSAIC_CREDENTIALS_FILE). # # Supported services: # portainer, coolify, authentik, glpi, github, # gitea-mosaicstack, gitea-usc, woodpecker, cloudflare # # After loading, service-specific env vars are exported. # Run `load_credentials --help` for details. MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" _mosaic_require_jq() { if ! command -v jq &>/dev/null; then echo "Error: jq is required but not installed" >&2 return 1 fi } _mosaic_read_cred() { local jq_path="$1" if [[ ! -f "$MOSAIC_CREDENTIALS_FILE" ]]; then echo "Error: Credentials file not found: $MOSAIC_CREDENTIALS_FILE" >&2 return 1 fi jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE" } load_credentials() { local service="$1" if [[ -z "$service" || "$service" == "--help" ]]; then cat <<'EOF' Usage: load_credentials Services and exported variables: portainer → PORTAINER_URL, PORTAINER_API_KEY coolify → COOLIFY_URL, COOLIFY_TOKEN authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_USERNAME, AUTHENTIK_PASSWORD glpi → GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN github → GITHUB_TOKEN gitea-mosaicstack → GITEA_URL, GITEA_TOKEN gitea-usc → GITEA_URL, GITEA_TOKEN woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN (uses default instance) woodpecker- → WOODPECKER_URL, WOODPECKER_TOKEN (specific instance, e.g. woodpecker-usc) cloudflare → CLOUDFLARE_API_TOKEN (uses default instance) cloudflare- → CLOUDFLARE_API_TOKEN (specific instance, e.g. cloudflare-personal) EOF return 0 fi _mosaic_require_jq || return 1 case "$service" in portainer) export PORTAINER_URL="${PORTAINER_URL:-$(_mosaic_read_cred '.portainer.url')}" export PORTAINER_API_KEY="${PORTAINER_API_KEY:-$(_mosaic_read_cred '.portainer.api_key')}" PORTAINER_URL="${PORTAINER_URL%/}" [[ -n "$PORTAINER_URL" ]] || { echo "Error: portainer.url not found" >&2; return 1; } [[ -n "$PORTAINER_API_KEY" ]] || { echo "Error: portainer.api_key not found" >&2; return 1; } ;; coolify) export COOLIFY_URL="${COOLIFY_URL:-$(_mosaic_read_cred '.coolify.url')}" export COOLIFY_TOKEN="${COOLIFY_TOKEN:-$(_mosaic_read_cred '.coolify.app_token')}" COOLIFY_URL="${COOLIFY_URL%/}" [[ -n "$COOLIFY_URL" ]] || { echo "Error: coolify.url not found" >&2; return 1; } [[ -n "$COOLIFY_TOKEN" ]] || { echo "Error: coolify.app_token not found" >&2; return 1; } ;; authentik) export AUTHENTIK_URL="${AUTHENTIK_URL:-$(_mosaic_read_cred '.authentik.url')}" export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}" export AUTHENTIK_USERNAME="${AUTHENTIK_USERNAME:-$(_mosaic_read_cred '.authentik.username')}" export AUTHENTIK_PASSWORD="${AUTHENTIK_PASSWORD:-$(_mosaic_read_cred '.authentik.password')}" AUTHENTIK_URL="${AUTHENTIK_URL%/}" [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; } ;; glpi) export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}" export GLPI_APP_TOKEN="${GLPI_APP_TOKEN:-$(_mosaic_read_cred '.glpi.app_token')}" export GLPI_USER_TOKEN="${GLPI_USER_TOKEN:-$(_mosaic_read_cred '.glpi.user_token')}" GLPI_URL="${GLPI_URL%/}" [[ -n "$GLPI_URL" ]] || { echo "Error: glpi.url not found" >&2; return 1; } ;; github) export GITHUB_TOKEN="${GITHUB_TOKEN:-$(_mosaic_read_cred '.github.token')}" [[ -n "$GITHUB_TOKEN" ]] || { echo "Error: github.token not found" >&2; return 1; } ;; gitea-mosaicstack) export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}" export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}" GITEA_URL="${GITEA_URL%/}" [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; } [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; } ;; gitea-usc) export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}" export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}" GITEA_URL="${GITEA_URL%/}" [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; } [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; } ;; woodpecker-*) local wp_instance="${service#woodpecker-}" export WOODPECKER_URL="${WOODPECKER_URL:-$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")}" export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")}" export WOODPECKER_INSTANCE="$wp_instance" WOODPECKER_URL="${WOODPECKER_URL%/}" [[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.${wp_instance}.url not found" >&2; return 1; } [[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.${wp_instance}.token not found" >&2; return 1; } ;; woodpecker) # Resolve default instance, then load it local wp_default wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}" if [[ -z "$wp_default" ]]; then # Fallback: try legacy flat structure (.woodpecker.url / .woodpecker.token) local legacy_url legacy_url="$(_mosaic_read_cred '.woodpecker.url')" if [[ -n "$legacy_url" ]]; then export WOODPECKER_URL="${WOODPECKER_URL:-$legacy_url}" export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}" WOODPECKER_URL="${WOODPECKER_URL%/}" [[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; } [[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; } else echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2 echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2 return 1 fi else load_credentials "woodpecker-${wp_default}" fi ;; cloudflare-*) local cf_instance="${service#cloudflare-}" export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-$(_mosaic_read_cred ".cloudflare.${cf_instance}.api_token")}" export CLOUDFLARE_INSTANCE="$cf_instance" [[ -n "$CLOUDFLARE_API_TOKEN" ]] || { echo "Error: cloudflare.${cf_instance}.api_token not found" >&2; return 1; } ;; cloudflare) # Resolve default instance, then load it local cf_default cf_default="${CLOUDFLARE_INSTANCE:-$(_mosaic_read_cred '.cloudflare.default')}" if [[ -z "$cf_default" ]]; then echo "Error: cloudflare.default not set and no CLOUDFLARE_INSTANCE env var" >&2 return 1 fi load_credentials "cloudflare-${cf_default}" ;; *) echo "Error: Unknown service '$service'" >&2 echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-], cloudflare[-]" >&2 return 1 ;; esac } # Common HTTP helper — makes a curl request and separates body from status code # Usage: mosaic_http GET "/api/v1/endpoint" "Authorization: Bearer $TOKEN" [base_url] # Returns: body on stdout, sets MOSAIC_HTTP_CODE mosaic_http() { local method="$1" local endpoint="$2" local auth_header="$3" local base_url="${4:-}" local response response=$(curl -sk -w "\n%{http_code}" -X "$method" \ -H "$auth_header" \ -H "Content-Type: application/json" \ "${base_url}${endpoint}") MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) echo "$response" | sed '$d' } # POST variant with body # Usage: mosaic_http_post "/api/v1/endpoint" "Authorization: Bearer $TOKEN" '{"key":"val"}' [base_url] mosaic_http_post() { local endpoint="$1" local auth_header="$2" local data="$3" local base_url="${4:-}" local response response=$(curl -sk -w "\n%{http_code}" -X POST \ -H "$auth_header" \ -H "Content-Type: application/json" \ -d "$data" \ "${base_url}${endpoint}") MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) echo "$response" | sed '$d' } # PATCH variant with body mosaic_http_patch() { local endpoint="$1" local auth_header="$2" local data="$3" local base_url="${4:-}" local response response=$(curl -sk -w "\n%{http_code}" -X PATCH \ -H "$auth_header" \ -H "Content-Type: application/json" \ -d "$data" \ "${base_url}${endpoint}") MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) echo "$response" | sed '$d' }