#!/usr/bin/env bash # gstack-auth — authenticate with Supabase via email OTP # # Usage: # gstack-auth [email] — start auth flow (prompts if no email) # gstack-auth status — show current auth status # gstack-auth logout — remove saved tokens # # Sends a 6-digit verification code to the user's email. # User enters the code in the terminal to authenticate. # # Env overrides (for testing): # GSTACK_STATE_DIR — override ~/.gstack state directory # GSTACK_DIR — override auto-detected gstack root set -euo pipefail GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}" AUTH_FILE="$STATE_DIR/auth-token.json" # Source Supabase config if [ -f "$GSTACK_DIR/supabase/config.sh" ]; then . "$GSTACK_DIR/supabase/config.sh" fi SUPABASE_URL="${GSTACK_SUPABASE_URL:-}" ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}" if [ -z "$SUPABASE_URL" ] || [ -z "$ANON_KEY" ]; then echo "Error: Supabase not configured. Check supabase/config.sh" exit 1 fi AUTH_URL="${SUPABASE_URL}/auth/v1" # ─── Helper: write auth token file ────────────────────────── save_token() { local access_token="$1" local refresh_token="$2" local expires_in="$3" local email="$4" local user_id="$5" local expires_at expires_at=$(( $(date +%s) + expires_in )) mkdir -p "$STATE_DIR" cat > "$AUTH_FILE" <" exit 0 fi AUTH_JSON="$(cat "$AUTH_FILE")" EMAIL="$(json_field "$AUTH_JSON" "email")" EXPIRES_AT="$(json_field "$AUTH_JSON" "expires_at")" NOW="$(date +%s)" if [ "$NOW" -lt "$EXPIRES_AT" ] 2>/dev/null; then REMAINING=$(( (EXPIRES_AT - NOW) / 60 )) echo "Authenticated as: $EMAIL" echo "Token expires in: ${REMAINING}m" else echo "Authenticated as: $EMAIL (token expired — will auto-refresh)" fi exit 0 fi # ─── Subcommand: logout ───────────────────────────────────── if [ "${1:-}" = "logout" ]; then rm -f "$AUTH_FILE" echo "Logged out. Auth token removed." exit 0 fi # ─── Main: auth flow ──────────────────────────────────────── EMAIL="${1:-}" if [ -z "$EMAIL" ]; then printf "Enter your email: " read -r EMAIL fi if [ -z "$EMAIL" ]; then echo "Error: email is required" exit 1 fi if ! echo "$EMAIL" | grep -qE '^[^@]+@[^@]+\.[^@]+$'; then echo "Error: invalid email format" exit 1 fi # ─── Step 1: Send OTP ──────────────────────────────────────── echo "" echo "Sending verification code to ${EMAIL}..." OTP_BODY="{\"email\":\"${EMAIL}\"}" HTTP_RESPONSE="$(curl -s -w "\n%{http_code}" \ -X POST "${AUTH_URL}/otp" \ -H "Content-Type: application/json" \ -H "apikey: ${ANON_KEY}" \ -d "$OTP_BODY" 2>/dev/null || echo -e "\n000")" HTTP_CODE="$(echo "$HTTP_RESPONSE" | tail -1)" HTTP_BODY="$(echo "$HTTP_RESPONSE" | sed '$d')" case "$HTTP_CODE" in 2*) ;; # success 429) if echo "$HTTP_BODY" | grep -q "email_send_rate_limit"; then echo "" echo "Email rate limit exceeded (Supabase free tier: ~3 emails/hour)." echo "Try again in a few minutes, or set up custom SMTP in the Supabase" echo "dashboard for unlimited sends." exit 1 fi echo "Cooldown active — waiting 60s before retrying..." for i in $(seq 60 -1 1); do printf "\r Retrying in %2ds..." "$i" sleep 1 done printf "\r \r" echo "Retrying..." HTTP_RESPONSE="$(curl -s -w "\n%{http_code}" \ -X POST "${AUTH_URL}/otp" \ -H "Content-Type: application/json" \ -H "apikey: ${ANON_KEY}" \ -d "$OTP_BODY" 2>/dev/null || echo -e "\n000")" HTTP_CODE="$(echo "$HTTP_RESPONSE" | tail -1)" HTTP_BODY="$(echo "$HTTP_RESPONSE" | sed '$d')" case "$HTTP_CODE" in 2*) ;; # success on retry *) echo "Error sending OTP (HTTP ${HTTP_CODE}): ${HTTP_BODY}"; exit 1 ;; esac ;; *) echo "Error sending OTP (HTTP ${HTTP_CODE}): ${HTTP_BODY}" exit 1 ;; esac echo "" echo "Check your email for a 6-digit code." echo "" # ─── Step 2: Read OTP code ─────────────────────────────────── printf "Enter code: " read -r OTP_CODE if [ -z "$OTP_CODE" ]; then echo "No code entered." exit 1 fi # ─── Step 3: Verify OTP ───────────────────────────────────── OTP_CODE="$(echo "$OTP_CODE" | tr -d '[:space:]')" if ! echo "$OTP_CODE" | grep -qE '^[0-9]{6}$'; then echo "Error: code must be exactly 6 digits" exit 1 fi VERIFY_RESPONSE="$(curl -s \ -X POST "${AUTH_URL}/verify" \ -H "Content-Type: application/json" \ -H "apikey: ${ANON_KEY}" \ -d "{\"email\":\"${EMAIL}\",\"token\":\"${OTP_CODE}\",\"type\":\"email\"}" \ 2>/dev/null || echo "{}")" ACCESS_TOKEN="$(json_field "$VERIFY_RESPONSE" "access_token")" REFRESH_TOKEN="$(json_field "$VERIFY_RESPONSE" "refresh_token")" EXPIRES_IN="$(json_field "$VERIFY_RESPONSE" "expires_in")" USER_ID="$(json_field "$VERIFY_RESPONSE" "id" 2>/dev/null || true)" if [ -z "$USER_ID" ]; then USER_ID="$(echo "$VERIFY_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | sed 's/"id":"//;s/"//')" fi if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then ERROR_MSG="$(json_field "$VERIFY_RESPONSE" "error_description" 2>/dev/null || json_field "$VERIFY_RESPONSE" "msg" 2>/dev/null || echo "unknown error")" echo "" echo "Verification failed: $ERROR_MSG" echo "Check the code and try again." exit 1 fi save_token "$ACCESS_TOKEN" "$REFRESH_TOKEN" "${EXPIRES_IN:-3600}" "$EMAIL" "$USER_ID" # ─── Step 4: Save email to config ──────────────────────────── "$GSTACK_DIR/bin/gstack-config" set email "$EMAIL" echo "" echo "Authenticated as: ${EMAIL}" echo "Token saved to: ${AUTH_FILE}"