diff --git a/Justfile b/Justfile index 23dec4b..053edb8 100644 --- a/Justfile +++ b/Justfile @@ -7,6 +7,12 @@ set dotenv-load := true dc_dev := "docker compose --env-file .env -f infra/docker-compose.dev.yml" dc_prod := "docker compose --env-file .env -f infra/docker-compose.prod.yml" +# ── Setup ───────────────────────────────────────────────────────────────────── + +# First-time (and idempotent) local environment setup +setup: + bash setup.sh + # ── Dev ─────────────────────────────────────────────────────────────────────── # Start full local stack (Docker services + SvelteKit dev server) diff --git a/apps/web/.env.development b/apps/web/.env.development index 268c789..4af038f 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -1,7 +1,7 @@ # Default public env vars for local development. # Committed — these are not secrets. Secret vars (.env) remain gitignored. PUBLIC_SUPABASE_URL=http://localhost:8001 -PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNDc0MDAwMCwiZXhwIjo0NzkwNDEzNjAwfQ.F70ylwDTR13K6X_jOWkGFdYNdXrQlzIFz6gbN4aqaMM +PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNDc0MDAwMCwiZXhwIjo0NzkwNDEzNjAwfQ.NRJAsCq6gp4kkP2f7bMxQmfbFWrZ6xy7r8btkIMK_h4 PUBLIC_KEYCLOAK_URL=http://keycloak:8080 PUBLIC_KEYCLOAK_REALM=colectivo PUBLIC_KEYCLOAK_CLIENT_ID=colectivo-web diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..0259df6 --- /dev/null +++ b/setup.sh @@ -0,0 +1,287 @@ +#!/usr/bin/env bash +# setup.sh — Prepare the local development environment. +# Safe to run multiple times: every step is a no-op if already complete. +# +# What it does: +# 1. Checks prerequisites (docker, pnpm); installs just if missing +# 2. Adds 127.0.0.1 keycloak to /etc/hosts (needed for GoTrue ↔ Keycloak) +# 3. Creates .env with dev defaults if it doesn't exist +# 4. Fixes supabase/migrations permissions if root-owned (common Docker artifact) +# 5. Installs pnpm dependencies +# 6. Starts the Docker stack +# 7. Waits for postgres, kong, and keycloak to be healthy +# 8. Applies SQL migrations (tracked — won't re-apply) +# 9. Seeds the database (no-op if already seeded) +# 10. Compiles Paraglide i18n files +set -euo pipefail + +# ── Colours ─────────────────────────────────────────────────────────────────── +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +step() { echo; echo -e "${BOLD}${BLUE}==> $*${NC}"; } +ok() { echo -e " ${GREEN}✓${NC} $*"; } +warn() { echo -e " ${YELLOW}!${NC} $*"; } +die() { echo -e "\n${RED}ERROR:${NC} $*" >&2; exit 1; } + +# ── Resolve project root ────────────────────────────────────────────────────── +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +DC="docker compose --env-file .env -f infra/docker-compose.dev.yml" +DB_CONTAINER="colectivo-dev-db-1" + +# ── 1. Prerequisites ────────────────────────────────────────────────────────── +step "Checking prerequisites" + +command -v docker >/dev/null 2>&1 \ + || die "docker not found. Install from https://docs.docker.com/get-docker/" +docker compose version >/dev/null 2>&1 \ + || die "docker compose v2 not found. Update Docker or install the compose plugin." +ok "docker $(docker --version | grep -oP '[\d.]+' | head -1)" + +command -v pnpm >/dev/null 2>&1 \ + || die "pnpm not found. Install: curl -fsSL https://get.pnpm.io/install.sh | sh" +ok "pnpm $(pnpm --version)" + +command -v python3 >/dev/null 2>&1 \ + || die "python3 not found — required to generate JWT tokens." + +if ! command -v just >/dev/null 2>&1; then + warn "just not found — installing to /usr/local/bin (requires sudo)" + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \ + | sudo bash -s -- --to /usr/local/bin + ok "just $(just --version)" +else + ok "just $(just --version)" +fi + +# ── 2. /etc/hosts ───────────────────────────────────────────────────────────── +step "Checking /etc/hosts for 'keycloak' → 127.0.0.1" + +if grep -qE '^\s*127\.0\.0\.1\s+.*\bkeycloak\b' /etc/hosts 2>/dev/null; then + ok "Entry already present" +else + warn "Adding '127.0.0.1 keycloak' to /etc/hosts (requires sudo)" + echo "127.0.0.1 keycloak" | sudo tee -a /etc/hosts >/dev/null + ok "Entry added" +fi + +# ── 3. .env ──────────────────────────────────────────────────────────────────── +step "Setting up .env" + +if [[ -f .env ]]; then + ok ".env already exists — skipping" +else + warn ".env not found — generating with dev defaults" + + # Well-known dev-only JWT secret. Never use in production. + JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long" + + # Generate anon and service_role JWTs signed with the dev secret. + # Python 3 is available on virtually every Linux/macOS dev machine. + IFS=' ' read -r ANON_KEY SERVICE_KEY < <(python3 - < .env < [timeout_seconds] +wait_for() { + local label="$1" + local cmd="$2" + local max="${3:-90}" + local n=0 + printf " %-18s" "$label" + until eval "$cmd" >/dev/null 2>&1; do + n=$((n + 2)) + if (( n >= max )); then + echo -e " ${RED}timed out${NC}" + die "$label did not become healthy after ${max}s. Check: docker compose -f infra/docker-compose.dev.yml logs" + fi + sleep 2 + printf "." + done + echo -e " ${GREEN}ready${NC}" +} + +# PostgreSQL must be first — everything else depends on it. +wait_for "postgres" "docker exec ${DB_CONTAINER} pg_isready -U postgres -q" +# GoTrue (auth) is reached through Kong. +wait_for "kong/auth" "curl -sf http://localhost:8001/auth/v1/health" +# Keycloak realm endpoint — ready only after realm import completes. +wait_for "keycloak" "curl -sf http://localhost:8080/realms/colectivo" 120 + +# ── 8. Apply database migrations ────────────────────────────────────────────── +step "Applying database migrations" + +# Create migration tracking table in the public schema (idempotent). +docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \ + -c "CREATE TABLE IF NOT EXISTS public._applied_migrations ( + filename TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ DEFAULT now() + );" \ + 2>/dev/null || die "Could not create migration tracking table" + +shopt -s nullglob +migration_files=(supabase/migrations/*.sql) +shopt -u nullglob + +if (( ${#migration_files[@]} == 0 )); then + warn "No migration files found in supabase/migrations/" +else + for sql_file in "${migration_files[@]}"; do + filename="$(basename "$sql_file")" + + already_applied=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \ + -c "SELECT count(*) FROM public._applied_migrations WHERE filename = '${filename}'" \ + 2>/dev/null) + + if [[ "${already_applied:-0}" -gt 0 ]]; then + ok "$filename (already applied)" + continue + fi + + printf " Applying %-40s" "$filename ..." + if docker exec -i "$DB_CONTAINER" \ + psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \ + < "$sql_file" 2>/tmp/migration_err; then + docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \ + -c "INSERT INTO public._applied_migrations (filename) + VALUES ('${filename}') ON CONFLICT DO NOTHING;" + echo -e " ${GREEN}done${NC}" + else + echo -e " ${RED}FAILED${NC}" + echo "--- error output ---" + cat /tmp/migration_err + die "Migration ${filename} failed. Fix the error and re-run setup.sh." + fi + done +fi + +# ── 9. Seed database ─────────────────────────────────────────────────────────── +step "Seeding database" + +user_count=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \ + -c "SELECT count(*) FROM public.users" 2>/dev/null || echo "0") + +if (( user_count > 0 )); then + ok "Already seeded ($user_count users in public.users)" +else + printf " Applying seed.sql ..." + if docker exec -i "$DB_CONTAINER" \ + psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \ + < supabase/seed.sql 2>/tmp/seed_err; then + echo -e " ${GREEN}done${NC}" + else + echo -e " ${RED}FAILED${NC}" + cat /tmp/seed_err + die "seed.sql failed. Fix the error and re-run setup.sh." + fi +fi + +# ── 10. Compile Paraglide i18n ────────────────────────────────────────────── +step "Compiling Paraglide i18n" + +PARAGLIDE_OUTDIR="apps/web/src/lib/paraglide" + +if [[ -d "$PARAGLIDE_OUTDIR" && -f "$PARAGLIDE_OUTDIR/runtime.js" ]]; then + ok "Already compiled" +else + PARAGLIDE_BIN="$(find node_modules/.pnpm -name 'paraglide-js' -path '*/.bin/paraglide-js' \ + 2>/dev/null | head -1)" + + if [[ -z "$PARAGLIDE_BIN" ]]; then + warn "paraglide-js binary not found — skipping (run 'pnpm install' and re-run setup.sh)" + else + "$PARAGLIDE_BIN" compile \ + --project apps/web/project.inlang \ + --outdir "$PARAGLIDE_OUTDIR" + ok "Compiled to $PARAGLIDE_OUTDIR" + fi +fi + +# ── Done ─────────────────────────────────────────────────────────────────────── +echo +echo -e "${BOLD}${GREEN}✓ Local environment is ready.${NC}" +echo +echo " Start the app: just dev" +echo " Supabase Studio: http://localhost:54323" +echo " Keycloak Admin: http://localhost:8080/admin (admin / admin)" +echo " Test users (pw: test1234): ana, borja, carmen, david, eva" +echo