diff --git a/.env.example b/.env.example index eefc6b46..94cd0a87 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ APP_NAME=Laravel APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=https://whispermoney.test +APP_URL=https://whisper.money.local APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -21,7 +21,7 @@ LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=mysql -DB_HOST=127.0.0.1 +DB_HOST=mysql DB_PORT=3306 DB_DATABASE=whisper_money DB_USERNAME=root @@ -43,7 +43,7 @@ CACHE_STORE=database MEMCACHED_HOST=127.0.0.1 REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 +REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 diff --git a/.gitignore b/.gitignore index 0b92df8b..88259f56 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ yarn-error.log /.zed .php-cs-fixer.cache /traefik/certs/*.pem +/docker/caddy/certs/*.pem +/docker/caddy/certs/*-key.pem .claude/settings.local.json diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..1225c488 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,16 @@ +whisper.money.local { + # Use mkcert certificates (generated by setup script) + # These certificates are automatically trusted by browsers + tls /etc/caddy/certs/whisper.money.local.pem /etc/caddy/certs/whisper.money.local-key.pem + + reverse_proxy php:8000 + + header { + -Server + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + X-XSS-Protection "1; mode=block" + } + + encode zstd gzip +} diff --git a/Dockerfile b/Dockerfile index 249e824d..e7a9e126 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ -# Use the official PHP 8.4 FPM image -FROM php:8.4-fpm +FROM php:8.4-cli -# Install system dependencies RUN apt-get update && apt-get install -y \ git \ curl \ @@ -12,71 +10,17 @@ RUN apt-get update && apt-get install -y \ libicu-dev \ zip \ unzip \ - nginx \ - supervisor \ - libmemcached-dev \ - zlib1g-dev \ + procps \ + net-tools \ + netcat-openbsd \ && rm -rf /var/lib/apt/lists/* -# Install PHP extensions RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip intl -RUN pecl install memcached redis \ - && docker-php-ext-enable memcached redis +RUN pecl install redis \ + && docker-php-ext-enable redis -# Install Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer -# Install Node.js -RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ - && apt-get install -y nodejs - -# Install Bun -RUN curl -fsSL https://bun.com/install | bash -ENV PATH="/root/.bun/bin:${PATH}" - -# Install memcached and redis servers -RUN apt-get update && apt-get install -y memcached redis-server && rm -rf /var/lib/apt/lists/* - -# Set working directory WORKDIR /app -# Copy application files -COPY . /app/. - -# Create necessary directories -RUN mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx - -# Install PHP dependencies -RUN composer install --no-dev --optimize-autoloader - -# Install Node.js dependencies -RUN bun install --frozen-lockfile - -# Build assets with HIDE_AUTH_BUTTONS=false so Wayfinder generates all routes -# (HIDE_AUTH_BUTTONS will be set to true at runtime via .env) -RUN HIDE_AUTH_BUTTONS=false bun run build:ssr - -# Copy supervisor configuration -COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf - -# Copy nginx configuration -COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf - -# Set permissions -RUN chown -R www-data:www-data /app \ - && chmod -R 755 /app/storage \ - && chmod -R 755 /app/bootstrap/cache - -# Expose port -EXPOSE 80 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost/up || exit 1 - -# Copy entrypoint script -COPY docker/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -# Run entrypoint script (handles migrations, caching, then starts supervisor) -CMD ["/entrypoint.sh"] +CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"] diff --git a/Dockerfile.production b/Dockerfile.production new file mode 100644 index 00000000..249e824d --- /dev/null +++ b/Dockerfile.production @@ -0,0 +1,82 @@ +# Use the official PHP 8.4 FPM image +FROM php:8.4-fpm + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + libzip-dev \ + libicu-dev \ + zip \ + unzip \ + nginx \ + supervisor \ + libmemcached-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions +RUN docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd zip intl +RUN pecl install memcached redis \ + && docker-php-ext-enable memcached redis + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Install Node.js +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs + +# Install Bun +RUN curl -fsSL https://bun.com/install | bash +ENV PATH="/root/.bun/bin:${PATH}" + +# Install memcached and redis servers +RUN apt-get update && apt-get install -y memcached redis-server && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy application files +COPY . /app/. + +# Create necessary directories +RUN mkdir -p /var/log/nginx && mkdir -p /var/cache/nginx + +# Install PHP dependencies +RUN composer install --no-dev --optimize-autoloader + +# Install Node.js dependencies +RUN bun install --frozen-lockfile + +# Build assets with HIDE_AUTH_BUTTONS=false so Wayfinder generates all routes +# (HIDE_AUTH_BUTTONS will be set to true at runtime via .env) +RUN HIDE_AUTH_BUTTONS=false bun run build:ssr + +# Copy supervisor configuration +COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Copy nginx configuration +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf + +# Set permissions +RUN chown -R www-data:www-data /app \ + && chmod -R 755 /app/storage \ + && chmod -R 755 /app/bootstrap/cache + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost/up || exit 1 + +# Copy entrypoint script +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Run entrypoint script (handles migrations, caching, then starts supervisor) +CMD ["/entrypoint.sh"] diff --git a/README.md b/README.md index 7a5b0723..bc54c143 100644 --- a/README.md +++ b/README.md @@ -44,40 +44,54 @@ Whisper Money is a privacy-first personal finance application that helps you tra ## Running Locally -### Prerequisites +### Quick Start (Recommended) -- Docker & Docker Compose -- Composer -- Node.js / Bun +The easiest way to get started is using our automated setup script: -### Setup +```bash +bash <(curl -fsSL https://whisper.money/setup.sh) +``` + +After installation, just visit **https://whisper.money.local** in your browser. + +### Manual Setup + +If you prefer to set up manually: 1. **Clone the repository:** ```bash git clone https://github.com/whisper-money/whisper-money.git -cd whisper_money +cd whisper-money ``` -2. **Copy the environment file:** +2. **Run the setup script:** ```bash -cp .env.example .env +./setup.sh install ``` -3. **Start the Docker services:** +### Available Commands + +Once installed, you can use the setup script for common tasks: ```bash -docker compose up -d +# Start all services +./setup.sh start + +# Stop all services +./setup.sh stop + +# Upgrade to latest version +./setup.sh upgrade + +# Interactive menu +./setup.sh ``` -4. **Install dependencies and setup the application:** +### Development Server -```bash -composer setup -``` - -5. **Start the development server:** +For active development with hot reloading: ```bash composer run dev @@ -90,7 +104,7 @@ This will concurrently start: - Log viewer (Pail) - Vite dev server -The application will be available at `https://whispermoney.test`. +The application will be available at **https://whisper.money.local** (via Caddy) or **http://localhost:8000** (direct PHP server). ## Running with Docker (Production Image) diff --git a/compose.yaml b/compose.yaml index 7931a037..7cd39fc1 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,27 +1,20 @@ services: - traefik: - image: 'traefik:v3.0' - command: - - '--api.insecure=true' - - '--providers.docker=true' - - '--providers.docker.exposedbydefault=false' - - '--providers.file.directory=/etc/traefik/dynamic' - - '--providers.file.watch=true' - - '--entrypoints.web.address=:80' - - '--entrypoints.websecure.address=:443' - - '--log.level=INFO' + caddy: + image: 'caddy:latest' ports: - '80:80' - '443:443' - - '8080:8080' + - '443:443/udp' volumes: - - '/var/run/docker.sock:/var/run/docker.sock:ro' - - './traefik/dynamic:/etc/traefik/dynamic:ro' - - './traefik/certs:/etc/traefik/certs:ro' + - './Caddyfile:/etc/caddy/Caddyfile:ro' + - './docker/caddy/certs:/etc/caddy/certs:ro' + - 'caddy_data:/data' + - 'caddy_config:/config' networks: - sail extra_hosts: - 'host.docker.internal:host-gateway' + restart: unless-stopped mysql: image: 'mysql/mysql-server:8.0' ports: @@ -69,6 +62,32 @@ services: - '${FORWARD_MAILHOG_DASHBOARD_PORT:-8025}:8025' networks: - sail + php: + build: + context: . + dockerfile: Dockerfile + ports: + - '8000:8000' + volumes: + - '.:/app' + - './vendor:/app/vendor' + - './node_modules:/app/node_modules' + env_file: + - .env + environment: + - DB_HOST=mysql + - DB_PORT=3306 + - REDIS_HOST=redis + - REDIS_PORT=6379 + networks: + - sail + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + command: php artisan serve --host=0.0.0.0 --port=8000 networks: sail: driver: bridge @@ -77,3 +96,7 @@ volumes: driver: local sail-redis: driver: local + caddy_data: + driver: local + caddy_config: + driver: local diff --git a/setup.sh b/setup.sh new file mode 100755 index 00000000..6eaea2e9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,1018 @@ +#!/bin/bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Repository URL +REPO_URL="https://github.com/whisper-money/whisper-money.git" +REPO_DIR="whisper-money" + +# Check if we're in a git repository or if the repo directory exists +check_and_setup_repo() { + # Check if we're already in the whisper-money git repo + if [ -d ".git" ] && git remote get-url origin 2>/dev/null | grep -q "whisper-money"; then + echo -e "${GREEN}✓ Already in Whisper Money repository${NC}" + return 0 + fi + + # Check if whisper-money directory exists in current directory + if [ -d "$REPO_DIR" ] && [ -d "$REPO_DIR/.git" ]; then + echo -e "${GREEN}✓ Repository found in ${REPO_DIR}${NC}" + echo -e "${YELLOW}Changing to ${REPO_DIR} directory...${NC}" + cd "$REPO_DIR" + # Re-execute the script from the new directory with original arguments + exec "./setup.sh" "$@" + fi + + # Not in repo and repo directory doesn't exist - prompt to clone + print_header + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Repository Setup${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}Whisper Money repository not found in the current directory.${NC}" + echo "" + echo -e "${YELLOW}We need to clone the repository to continue.${NC}" + echo "" + echo -e "${BLUE}What we'll do:${NC}" + echo -e " • Clone the repository into: ${GREEN}${REPO_DIR}${NC}" + echo -e " • Change into that directory" + echo -e " • Continue with installation" + echo "" + + # Check if git is available + if ! command_exists git; then + echo -e "${RED}Git is not installed. Please install Git first:${NC}" + if [[ "$OSTYPE" == "darwin"* ]]; then + echo -e " ${YELLOW}brew install git${NC}" + else + echo -e " ${YELLOW}sudo apt install git${NC} # Debian/Ubuntu" + fi + exit 1 + fi + + read -p "Clone the repository now? (y/n) " -n 1 -r + echo "" + echo "" + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Repository cloning cancelled.${NC}" + echo -e "${BLUE}You can clone it manually and run this script from the repository directory:${NC}" + echo -e " ${GREEN}git clone ${REPO_URL}${NC}" + echo -e " ${GREEN}cd ${REPO_DIR}${NC}" + echo -e " ${GREEN}./setup.sh${NC}" + exit 0 + fi + + # Clone the repository + echo -e "${YELLOW}Cloning Whisper Money repository...${NC}" + if git clone "$REPO_URL" "$REPO_DIR" 2>&1; then + echo -e "${GREEN}✓ Repository cloned successfully${NC}" + echo "" + echo -e "${YELLOW}Changing to ${REPO_DIR} directory...${NC}" + cd "$REPO_DIR" + + # Make sure the script is executable + if [ -f "setup.sh" ]; then + chmod +x setup.sh + fi + + echo -e "${GREEN}✓ Ready to continue with installation${NC}" + echo "" + + # Re-execute the script from the new directory + exec "./setup.sh" "$@" + else + echo -e "${RED}✗ Failed to clone repository${NC}" + echo -e "${YELLOW}Please check your internet connection and try again.${NC}" + exit 1 + fi +} + +# ASCII Art Header +print_header() { + echo -e "${BLUE}" + cat <<"EOF" +██╗ ██╗██╗ ██╗██╗███████╗██████╗ ███████╗██████╗ ███╗ ███╗ ██████╗ ███╗ ██╗███████╗██╗ ██╗ +██║ ██║██║ ██║██║██╔════╝██╔══██╗██╔════╝██╔══██╗ ████╗ ████║██╔═══██╗████╗ ██║██╔════╝╚██╗ ██╔╝ +██║ █╗ ██║███████║██║███████╗██████╔╝█████╗ ██████╔╝ ██╔████╔██║██║ ██║██╔██╗ ██║█████╗ ╚████╔╝ +██║███╗██║██╔══██║██║╚════██║██╔═══╝ ██╔══╝ ██╔══██╗ ██║╚██╔╝██║██║ ██║██║╚██╗██║██╔══╝ ╚██╔╝ +╚███╔███╔╝██║ ██║██║███████║██║ ███████╗██║ ██║ ██║ ╚═╝ ██║╚██████╔╝██║ ╚████║███████╗ ██║ + ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═╝ + +EOF + echo -e "${NC}" + echo -e "${GREEN}Whisper Money - Privacy-First Personal Finance App${NC}" + echo -e "${BLUE}End-to-end encrypted financial management${NC}" + echo "" +} + +# Spinner function for long operations +spinner() { + local pid=$1 + local message=$2 + local spin='-\|/' + local i=0 + while kill -0 $pid 2>/dev/null; do + i=$(((i + 1) % 4)) + printf "\r${YELLOW}${message} ${spin:$i:1}${NC}" + sleep .1 + done + printf "\r${GREEN}${message} ✓${NC}\n" +} + +# Check if command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if project is outdated +check_project_version() { + # Only check if we're in a git repository + if [ ! -d ".git" ]; then + return 0 + fi + + # Check if git is available + if ! command_exists git; then + return 0 + fi + + # Fetch latest changes without merging + git fetch origin >/dev/null 2>&1 || return 0 + + # Get current branch + local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main") + + # Compare local and remote commits + local local_commit=$(git rev-parse HEAD 2>/dev/null) + local remote_commit=$(git rev-parse "origin/${current_branch}" 2>/dev/null) + + if [ -z "$local_commit" ] || [ -z "$remote_commit" ]; then + return 0 + fi + + # Check if we're behind remote + if [ "$local_commit" != "$remote_commit" ]; then + local behind_count=$(git rev-list --count HEAD.."origin/${current_branch}" 2>/dev/null || echo "0") + + if [ "$behind_count" -gt 0 ]; then + echo "" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}⚠ Project Update Available${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}Your local project is ${behind_count} commit(s) behind the remote repository.${NC}" + echo "" + echo -e "${YELLOW}To update, run:${NC}" + echo -e " ${GREEN}./setup.sh upgrade${NC}" + echo "" + echo -e "${BLUE}Or manually:${NC}" + echo -e " ${GREEN}git pull${NC}" + echo "" + return 1 + fi + fi + + return 0 +} + +# Check dependencies +check_dependencies() { + local missing_deps=() + + if ! command_exists docker; then + missing_deps+=("Docker") + fi + + if ! command_exists docker-compose && ! docker compose version >/dev/null 2>&1; then + missing_deps+=("Docker Compose") + fi + + if ! command_exists php; then + missing_deps+=("PHP (>= 8.2)") + else + php_version=$(php -r 'echo PHP_VERSION_ID;') + if [ "$php_version" -lt 80200 ]; then + missing_deps+=("PHP >= 8.2 (found: $(php -r 'echo PHP_VERSION;'))") + fi + fi + + if ! command_exists composer; then + missing_deps+=("Composer") + fi + + if ! command_exists bun && ! command_exists node; then + missing_deps+=("Bun or Node.js") + fi + + if [ ${#missing_deps[@]} -ne 0 ]; then + echo -e "${RED}Missing dependencies:${NC}" + for dep in "${missing_deps[@]}"; do + echo -e " - ${dep}" + done + echo "" + echo -e "${YELLOW}Please install the missing dependencies and try again.${NC}" + exit 1 + fi + + echo -e "${GREEN}All dependencies are installed.${NC}" +} + +# Wait for service to be healthy +wait_for_service() { + local service=$1 + local max_attempts=60 + local attempt=1 + + echo -e "${YELLOW}Waiting for ${service} to be ready...${NC}" + + while [ $attempt -le $max_attempts ]; do + # Check container status + local ps_output=$(docker compose ps "$service" 2>/dev/null | grep "$service" || echo "") + local container_state=$(echo "$ps_output" | awk '{print $1}' || echo "") + + # Check if container is running + if echo "$ps_output" | grep -qE "Up|healthy"; then + # For PHP service, verify it's actually responding + if [ "$service" = "php" ]; then + # Give it a moment to fully start (especially on first attempt) + if [ $attempt -lt 5 ]; then + sleep 2 + else + sleep 1 + fi + + # Check if we can exec into the container and PHP works + if docker compose exec -T php php -v >/dev/null 2>&1; then + # Try multiple ways to check if server is running + # Method 1: Check for artisan serve process + if docker compose exec -T php sh -c "ps aux 2>/dev/null | grep '[p]hp.*artisan serve' >/dev/null 2>&1" 2>/dev/null; then + echo -e "${GREEN}${service} is ready!${NC}" + return 0 + fi + # Method 2: Check if port 8000 is listening + if docker compose exec -T php sh -c "netstat -tuln 2>/dev/null | grep ':8000' >/dev/null 2>&1 || ss -tuln 2>/dev/null | grep ':8000' >/dev/null 2>&1" 2>/dev/null; then + echo -e "${GREEN}${service} is ready!${NC}" + return 0 + fi + # Method 3: Try to connect to the server + if docker compose exec -T php sh -c "timeout 1 bash -c '/dev/null" 2>/dev/null; then + echo -e "${GREEN}${service} is ready!${NC}" + return 0 + fi + # If container is up and PHP works, but server check fails, still consider it ready after enough attempts + # (server might be starting) + if [ $attempt -ge 10 ]; then + echo -e "${YELLOW}${service} container is running but server check inconclusive. Proceeding...${NC}" + echo -e "${GREEN}${service} is ready!${NC}" + return 0 + fi + fi + else + echo -e "${GREEN}${service} is ready!${NC}" + return 0 + fi + elif echo "$ps_output" | grep -qE "Exit|exited|Dead"; then + # Container exited - show error + echo -e "${RED}${service} container exited unexpectedly!${NC}" + echo -e "${YELLOW}Checking logs...${NC}" + docker compose logs --tail=30 "$service" 2>&1 + return 1 + fi + + if [ $((attempt % 10)) -eq 0 ]; then + echo -e "${YELLOW}Still waiting... (${attempt}/${max_attempts})${NC}" + if [ "$service" = "php" ]; then + echo -e "${BLUE}Container status:${NC}" + docker compose ps php 2>/dev/null | grep php || echo " Container not found" + echo -e "${BLUE}Recent logs:${NC}" + docker compose logs --tail=5 php 2>&1 | tail -3 + fi + fi + + sleep 2 + attempt=$((attempt + 1)) + done + + echo -e "${RED}${service} failed to become ready after $max_attempts attempts${NC}" + echo -e "${YELLOW}Checking container status...${NC}" + docker compose ps "$service" + echo "" + echo -e "${YELLOW}Recent logs:${NC}" + docker compose logs --tail=30 "$service" 2>&1 | tail -20 + return 1 +} + +# Update /etc/hosts file +update_hosts_file() { + local hostname="whisper.money.local" + local ip="127.0.0.1" + local hosts_line="${ip} ${hostname}" + local hosts_file="/etc/hosts" + + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Hosts File Update${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}To access your application at ${GREEN}https://${hostname}${NC}${BLUE}, we need to${NC}" + echo -e "${BLUE}add an entry to your system's hosts file (${hosts_file}).${NC}" + echo "" + echo -e "${YELLOW}What we'll do:${NC}" + echo -e " • Add the line: ${GREEN}${hosts_line}${NC}" + echo -e " • This maps ${hostname} to your local machine (127.0.0.1)" + echo "" + echo -e "${YELLOW}Why we need sudo access:${NC}" + echo -e " • The hosts file is a system file that requires administrator privileges" + echo -e " • We'll only add the entry if it doesn't already exist (idempotent)" + echo "" + + # Check if the line already exists + if grep -q "${hostname}" "${hosts_file}" 2>/dev/null; then + if grep -q "${hosts_line}" "${hosts_file}" 2>/dev/null; then + echo -e "${GREEN}✓ The hosts entry already exists. No changes needed.${NC}" + return 0 + else + echo -e "${YELLOW}⚠ Found a different entry for ${hostname} in ${hosts_file}${NC}" + echo -e "${YELLOW} Please review it manually.${NC}" + echo "" + fi + fi + + read -p "Would you like us to update the hosts file automatically? (y/n) " -n 1 -r + echo "" + echo "" + + if [[ $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Updating hosts file (sudo access required)...${NC}" + + # Use sudo to add the line if it doesn't exist + if sudo sh -c "grep -q '${hosts_line}' ${hosts_file} || echo '${hosts_line}' >> ${hosts_file}"; then + echo -e "${GREEN}✓ Successfully updated ${hosts_file}${NC}" + echo -e "${GREEN} Added: ${hosts_line}${NC}" + return 0 + else + echo -e "${RED}✗ Failed to update ${hosts_file}${NC}" + echo -e "${YELLOW} You'll need to add it manually.${NC}" + show_manual_hosts_instructions + return 1 + fi + else + echo -e "${YELLOW}Hosts file update skipped.${NC}" + show_manual_hosts_instructions + return 1 + fi +} + +# Setup SSL certificates using mkcert for transparent trust +setup_ssl_certificates() { + local certs_dir="docker/caddy/certs" + local domain="whisper.money.local" + local cert_file="${certs_dir}/${domain}.pem" + local key_file="${certs_dir}/${domain}-key.pem" + + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}SSL Certificate Setup${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + + # Check if certificates already exist + if [ -f "$cert_file" ] && [ -f "$key_file" ]; then + echo -e "${GREEN}✓ SSL certificates already exist${NC}" + return 0 + fi + + # Check if mkcert is installed + if ! command_exists mkcert; then + echo -e "${YELLOW}mkcert is not installed. It's needed to generate trusted SSL certificates.${NC}" + echo "" + echo -e "${BLUE}mkcert creates locally-trusted certificates that browsers automatically trust.${NC}" + echo "" + + if [[ "$OSTYPE" == "darwin"* ]]; then + echo -e "${YELLOW}Install mkcert:${NC}" + echo -e " ${GREEN}brew install mkcert${NC}" + echo -e " ${GREEN}brew install nss${NC} # For Firefox support" + echo "" + read -p "Would you like to install mkcert now? (y/n) " -n 1 -r + echo "" + echo "" + + if [[ $REPLY =~ ^[Yy]$ ]]; then + if command_exists brew; then + echo -e "${YELLOW}Installing mkcert...${NC}" + brew install mkcert nss + echo -e "${GREEN}✓ mkcert installed${NC}" + else + echo -e "${RED}Homebrew is not installed. Please install mkcert manually:${NC}" + echo -e " ${YELLOW}brew install mkcert nss${NC}" + echo "" + echo -e "${YELLOW}Or visit: https://github.com/FiloSottile/mkcert#installation${NC}" + create_fallback_certificates + return 1 + fi + else + echo -e "${YELLOW}Using fallback self-signed certificates...${NC}" + create_fallback_certificates + return 0 + fi + else + echo -e "${YELLOW}Install mkcert:${NC}" + echo -e " ${GREEN}sudo apt install libnss3-tools${NC} # Debian/Ubuntu" + echo -e " ${GREEN}wget -O /tmp/mkcert https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-v1.4.4-linux-amd64${NC}" + echo -e " ${GREEN}chmod +x /tmp/mkcert && sudo mv /tmp/mkcert /usr/local/bin/mkcert${NC}" + echo "" + echo -e "${YELLOW}Or visit: https://github.com/FiloSottile/mkcert#installation${NC}" + echo "" + read -p "Continue with fallback self-signed certificates? (y/n) " -n 1 -r + echo "" + echo "" + + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Please install mkcert and run the setup again.${NC}" + exit 1 + fi + + create_fallback_certificates + return 0 + fi + fi + + # Install local CA if not already installed + if [ ! -f "$(mkcert -CAROOT)/rootCA.pem" ]; then + echo -e "${YELLOW}Installing mkcert local CA (requires sudo)...${NC}" + mkcert -install + echo -e "${GREEN}✓ Local CA installed${NC}" + else + echo -e "${GREEN}✓ Local CA already installed${NC}" + fi + + # Create certs directory + mkdir -p "$certs_dir" + + # Generate certificate + echo -e "${YELLOW}Generating SSL certificate for ${domain}...${NC}" + if mkcert -cert-file "$cert_file" -key-file "$key_file" "$domain" "*.${domain}"; then + echo -e "${GREEN}✓ SSL certificate generated successfully${NC}" + echo -e "${GREEN} Certificate: ${cert_file}${NC}" + echo -e "${GREEN} Key: ${key_file}${NC}" + echo "" + echo -e "${BLUE}Your browser will automatically trust this certificate!${NC}" + return 0 + else + echo -e "${RED}✗ Failed to generate certificate${NC}" + create_fallback_certificates + return 1 + fi +} + +# Create fallback self-signed certificates +create_fallback_certificates() { + local certs_dir="docker/caddy/certs" + local domain="whisper.money.local" + local cert_file="${certs_dir}/${domain}.pem" + local key_file="${certs_dir}/${domain}-key.pem" + + echo "" + echo -e "${YELLOW}Creating self-signed certificates (browser will show a warning)...${NC}" + + mkdir -p "$certs_dir" + + # Check if openssl is available + if command_exists openssl; then + # Create certificate with proper SAN extension + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout "$key_file" \ + -out "$cert_file" \ + -subj "/CN=${domain}/O=Development/C=US" \ + -addext "subjectAltName=DNS:${domain},DNS:*.${domain},IP:127.0.0.1" \ + 2>/dev/null + + if [ -f "$cert_file" ] && [ -f "$key_file" ]; then + # Set proper permissions + chmod 644 "$cert_file" + chmod 600 "$key_file" + echo -e "${GREEN}✓ Self-signed certificates created${NC}" + echo -e "${YELLOW} Note: Your browser will show a security warning.${NC}" + echo -e "${YELLOW} Click 'Advanced' → 'Proceed to ${domain}' to continue.${NC}" + return 0 + fi + fi + + echo -e "${RED}✗ Could not create certificates${NC}" + echo -e "${YELLOW} Please install mkcert or openssl to generate certificates.${NC}" + return 1 +} + +# Show manual hosts file instructions +show_manual_hosts_instructions() { + local hostname="whisper.money.local" + local hosts_line="127.0.0.1 ${hostname}" + + echo "" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}Manual Hosts File Update${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}To access your application, please add this line to ${YELLOW}/etc/hosts${NC}${BLUE}:${NC}" + echo "" + echo -e " ${GREEN}${hosts_line}${NC}" + echo "" + echo -e "${YELLOW}Instructions:${NC}" + echo -e " 1. Open ${YELLOW}/etc/hosts${NC} with a text editor (requires sudo):" + echo -e " ${YELLOW}sudo nano /etc/hosts${NC} ${BLUE}(or use your preferred editor)${NC}" + echo "" + echo -e " 2. Add the line at the end of the file:" + echo -e " ${GREEN}${hosts_line}${NC}" + echo "" + echo -e " 3. Save and close the file" + echo "" + echo -e "${YELLOW}Note:${NC} If the line already exists, you can skip this step." + echo "" +} + +# Start Docker services +start_services() { + echo -e "${BLUE}Starting Docker services...${NC}" + docker compose up -d + + wait_for_service "mysql" + wait_for_service "redis" + + # Only wait for PHP if it's supposed to be running + if docker compose ps php 2>/dev/null | grep -q "php"; then + wait_for_service "php" + fi + + echo -e "${GREEN}All services are running!${NC}" +} + +# Stop Docker services +stop_services() { + echo -e "${BLUE}Stopping Docker services...${NC}" + docker compose down + echo -e "${GREEN}Services stopped.${NC}" +} + +# Install function +install() { + print_header + + echo -e "${BLUE}Starting Whisper Money installation...${NC}" + echo "" + + # Check dependencies + check_dependencies + echo "" + + # Check if .env exists + if [ ! -f .env ]; then + echo -e "${YELLOW}.env file not found. Creating from .env.example...${NC}" + cp .env.example .env + echo -e "${GREEN}.env file created.${NC}" + else + echo -e "${GREEN}.env file already exists.${NC}" + fi + + # Generate APP_KEY if not set + if ! grep -q "APP_KEY=base64:" .env 2>/dev/null || grep -q "APP_KEY=$" .env 2>/dev/null; then + echo -e "${YELLOW}Generating application key...${NC}" + php artisan key:generate --no-interaction + echo -e "${GREEN}Application key generated.${NC}" + else + echo -e "${GREEN}Application key already exists.${NC}" + fi + + # Update APP_URL if needed + if grep -q "APP_URL=https://whispermoney.test" .env 2>/dev/null || grep -q "APP_URL=$" .env 2>/dev/null; then + echo -e "${YELLOW}Updating APP_URL to https://whisper.money.local...${NC}" + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's|APP_URL=.*|APP_URL=https://whisper.money.local|g' .env + else + sed -i 's|APP_URL=.*|APP_URL=https://whisper.money.local|g' .env + fi + echo -e "${GREEN}APP_URL updated.${NC}" + else + echo -e "${GREEN}APP_URL already configured.${NC}" + fi + + # Update database and Redis hosts/ports for Docker + echo -e "${YELLOW}Configuring database and Redis for Docker...${NC}" + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' 's|^DB_HOST=.*|DB_HOST=mysql|g' .env + sed -i '' 's|^DB_PORT=.*|DB_PORT=3306|g' .env + sed -i '' 's|^REDIS_HOST=.*|REDIS_HOST=redis|g' .env + sed -i '' 's|^REDIS_PORT=.*|REDIS_PORT=6379|g' .env + else + sed -i 's|^DB_HOST=.*|DB_HOST=mysql|g' .env + sed -i 's|^DB_PORT=.*|DB_PORT=3306|g' .env + sed -i 's|^REDIS_HOST=.*|REDIS_HOST=redis|g' .env + sed -i 's|^REDIS_PORT=.*|REDIS_PORT=6379|g' .env + fi + echo -e "${GREEN}Database and Redis configured for Docker.${NC}" + + echo "" + + # Setup SSL certificates before starting Caddy + setup_ssl_certificates + echo "" + + # Start Docker services (but don't start PHP yet - we'll start it after dependencies are installed) + echo -e "${BLUE}Starting Docker services (excluding PHP for now)...${NC}" + docker compose up -d mysql redis caddy mailhog + + wait_for_service "mysql" + wait_for_service "redis" + echo "" + + # Install Composer dependencies + echo -e "${BLUE}Installing Composer dependencies...${NC}" + composer install --no-interaction --prefer-dist --optimize-autoloader + echo -e "${GREEN}Composer dependencies installed.${NC}" + echo "" + + # Now start PHP service (after dependencies are installed) + echo -e "${BLUE}Starting PHP service...${NC}" + if ! docker compose up -d php; then + echo -e "${RED}Failed to start PHP service!${NC}" + echo -e "${YELLOW}Checking logs...${NC}" + docker compose logs php 2>&1 | tail -20 + exit 1 + fi + + # Wait a moment for PHP container to start + sleep 5 + + # Check if container started successfully + if ! docker compose ps php 2>/dev/null | grep -q "php"; then + echo -e "${RED}PHP container failed to start!${NC}" + echo -e "${YELLOW}Checking logs...${NC}" + docker compose logs php 2>&1 | tail -30 + exit 1 + fi + + wait_for_service "php" + echo "" + + # Install Bun/Node dependencies + if command_exists bun; then + echo -e "${BLUE}Installing Bun dependencies...${NC}" + bun install --frozen-lockfile + echo -e "${GREEN}Bun dependencies installed.${NC}" + elif command_exists npm; then + echo -e "${BLUE}Installing npm dependencies...${NC}" + npm ci + echo -e "${GREEN}npm dependencies installed.${NC}" + fi + echo "" + + # Build frontend assets with retry logic + echo -e "${BLUE}Building frontend assets...${NC}" + + # Small delay to ensure file system is ready + sleep 1 + + local build_success=false + local max_build_attempts=3 + local build_attempt=1 + + while [ $build_attempt -le $max_build_attempts ]; do + if command_exists bun; then + echo -e "${YELLOW}Build attempt ${build_attempt}/${max_build_attempts}...${NC}" + if bun run build 2>&1; then + build_success=true + break + else + if [ $build_attempt -lt $max_build_attempts ]; then + echo -e "${YELLOW}Build attempt ${build_attempt} failed. Retrying in 3 seconds...${NC}" + sleep 3 + fi + build_attempt=$((build_attempt + 1)) + fi + elif command_exists npm; then + echo -e "${YELLOW}Build attempt ${build_attempt}/${max_build_attempts}...${NC}" + if npm run build 2>&1; then + build_success=true + break + else + if [ $build_attempt -lt $max_build_attempts ]; then + echo -e "${YELLOW}Build attempt ${build_attempt} failed. Retrying in 3 seconds...${NC}" + sleep 3 + fi + build_attempt=$((build_attempt + 1)) + fi + else + echo -e "${RED}No build tool (bun/npm) found. Skipping build.${NC}" + break + fi + done + + if [ "$build_success" = false ]; then + echo -e "${RED}Failed to build frontend assets after ${max_build_attempts} attempts.${NC}" + echo "" + echo -e "${YELLOW}Troubleshooting tips:${NC}" + echo -e " 1. Try clearing caches and rebuilding:" + if command_exists bun; then + echo -e " ${YELLOW}rm -rf node_modules/.cache .vite && bun run build${NC}" + else + echo -e " ${YELLOW}rm -rf node_modules/.cache .vite && npm run build${NC}" + fi + echo -e " 2. Try reinstalling dependencies:" + if command_exists bun; then + echo -e " ${YELLOW}rm -rf node_modules && bun install${NC}" + else + echo -e " ${YELLOW}rm -rf node_modules && npm install${NC}" + fi + echo "" + echo -e "${YELLOW}You can also try building manually later with:${NC}" + if command_exists bun; then + echo -e " ${YELLOW}bun run build${NC}" + else + echo -e " ${YELLOW}npm run build${NC}" + fi + echo "" + read -p "Continue with installation anyway? (y/n) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${RED}Installation cancelled.${NC}" + exit 1 + fi + else + echo -e "${GREEN}Frontend assets built successfully.${NC}" + fi + echo "" + + # Run database migrations + echo -e "${BLUE}Running database migrations...${NC}" + docker compose exec -T php php artisan migrate --force + echo -e "${GREEN}Database migrations completed.${NC}" + echo "" + + # Update hosts file + update_hosts_file + + # Generate SSL certificates using mkcert + setup_ssl_certificates + + # Success message + echo "" + echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${GREEN} ✓ Whisper Money has been installed successfully!${NC}" + echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}Your application is now running!${NC}" + echo "" + echo -e " ${GREEN}Visit: https://whisper.money.local${NC}" + echo "" + + # Check if mkcert certificates are being used + if [ -f "docker/caddy/certs/whisper.money.local.pem" ]; then + echo -e "${GREEN}✓ Using trusted SSL certificates (mkcert)${NC}" + echo -e "${BLUE} Your browser will automatically trust the certificate!${NC}" + else + echo -e "${YELLOW}⚠ Using self-signed certificates${NC}" + echo -e "${BLUE} Your browser may show a security warning.${NC}" + echo -e "${BLUE} Click 'Advanced' → 'Proceed to whisper.money.local' to continue.${NC}" + echo "" + echo -e "${YELLOW}To avoid the warning, install mkcert and run setup again:${NC}" + if [[ "$OSTYPE" == "darwin"* ]]; then + echo -e " ${GREEN}brew install mkcert nss && mkcert -install${NC}" + else + echo -e " ${GREEN}# See: https://github.com/FiloSottile/mkcert#installation${NC}" + fi + fi + echo "" + echo -e "${BLUE}Services running:${NC}" + echo -e " • PHP development server (port 8000)" + echo -e " • Caddy reverse proxy (ports 80, 443)" + echo -e " • MySQL database" + echo -e " • Redis cache" + echo -e " • MailHog (email testing)" + echo "" + echo -e "${YELLOW}To stop all services:${NC}" + echo -e " ${YELLOW}./setup.sh stop${NC}" + echo "" + echo -e "${YELLOW}To start services again:${NC}" + echo -e " ${YELLOW}./setup.sh start${NC}" + echo "" +} + +# Upgrade function +upgrade() { + print_header + + echo -e "${BLUE}Upgrading Whisper Money...${NC}" + echo "" + + # Git pull + echo -e "${YELLOW}Pulling latest changes from Git...${NC}" + git pull + echo -e "${GREEN}Git pull completed.${NC}" + echo "" + + # Composer install + echo -e "${BLUE}Updating Composer dependencies...${NC}" + composer install --no-interaction --prefer-dist --optimize-autoloader + echo -e "${GREEN}Composer dependencies updated.${NC}" + echo "" + + # Bun/Node install + if command_exists bun; then + echo -e "${BLUE}Updating Bun dependencies...${NC}" + bun install --frozen-lockfile + echo -e "${GREEN}Bun dependencies updated.${NC}" + elif command_exists npm; then + echo -e "${BLUE}Updating npm dependencies...${NC}" + npm ci + echo -e "${GREEN}npm dependencies updated.${NC}" + fi + echo "" + + # Run migrations + echo -e "${BLUE}Running database migrations...${NC}" + docker compose exec -T php php artisan migrate --force + echo -e "${GREEN}Database migrations completed.${NC}" + echo "" + + # Rebuild assets with retry logic + echo -e "${BLUE}Rebuilding frontend assets...${NC}" + + # Small delay to ensure file system is ready + sleep 1 + + local build_success=false + local max_build_attempts=3 + local build_attempt=1 + + while [ $build_attempt -le $max_build_attempts ]; do + if command_exists bun; then + echo -e "${YELLOW}Build attempt ${build_attempt}/${max_build_attempts}...${NC}" + if bun run build 2>&1; then + build_success=true + break + else + if [ $build_attempt -lt $max_build_attempts ]; then + echo -e "${YELLOW}Build attempt ${build_attempt} failed. Retrying in 3 seconds...${NC}" + sleep 3 + fi + build_attempt=$((build_attempt + 1)) + fi + elif command_exists npm; then + echo -e "${YELLOW}Build attempt ${build_attempt}/${max_build_attempts}...${NC}" + if npm run build 2>&1; then + build_success=true + break + else + if [ $build_attempt -lt $max_build_attempts ]; then + echo -e "${YELLOW}Build attempt ${build_attempt} failed. Retrying in 3 seconds...${NC}" + sleep 3 + fi + build_attempt=$((build_attempt + 1)) + fi + else + echo -e "${RED}No build tool (bun/npm) found. Skipping build.${NC}" + break + fi + done + + if [ "$build_success" = false ]; then + echo -e "${RED}Failed to rebuild frontend assets after ${max_build_attempts} attempts.${NC}" + echo "" + echo -e "${YELLOW}Troubleshooting tips:${NC}" + echo -e " 1. Try clearing caches and rebuilding:" + if command_exists bun; then + echo -e " ${YELLOW}rm -rf node_modules/.cache .vite && bun run build${NC}" + else + echo -e " ${YELLOW}rm -rf node_modules/.cache .vite && npm run build${NC}" + fi + echo -e " 2. Try reinstalling dependencies:" + if command_exists bun; then + echo -e " ${YELLOW}rm -rf node_modules && bun install${NC}" + else + echo -e " ${YELLOW}rm -rf node_modules && npm install${NC}" + fi + echo "" + echo -e "${YELLOW}You can also try building manually later with:${NC}" + if command_exists bun; then + echo -e " ${YELLOW}bun run build${NC}" + else + echo -e " ${YELLOW}npm run build${NC}" + fi + echo "" + read -p "Continue with upgrade anyway? (y/n) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${RED}Upgrade cancelled.${NC}" + exit 1 + fi + else + echo -e "${GREEN}Frontend assets rebuilt successfully.${NC}" + fi + echo "" + + # Restart services + echo -e "${BLUE}Restarting services...${NC}" + docker compose restart + echo -e "${GREEN}Services restarted.${NC}" + echo "" + + echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${GREEN} ✓ Whisper Money has been upgraded successfully!${NC}" + echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" +} + +# Interactive menu +ask_for_action() { + print_header + + echo -e "${BLUE}What would you like to do?${NC}" + echo "" + echo " 1) Install" + echo " 2) Start Services" + echo " 3) Stop Services" + echo " 4) Upgrade" + echo " 5) Exit" + echo "" + read -p "Enter your choice [1-5]: " choice + + case $choice in + 1) + install + ;; + 2) + start_services + ;; + 3) + stop_services + ;; + 4) + upgrade + ;; + 5) + echo -e "${GREEN}Goodbye!${NC}" + exit 0 + ;; + *) + echo -e "${RED}Invalid choice. Please try again.${NC}" + ask_for_action + ;; + esac +} + +# Main script logic +main() { + # Check and setup repository first (only for install command or interactive mode) + # This will re-execute the script if cloning was needed + if [ "${1:-}" = "install" ] || [ -z "${1:-}" ]; then + check_and_setup_repo "$@" + fi + + # Check if project is outdated (skip for upgrade command) + if [ "${1:-}" != "upgrade" ]; then + check_project_version + fi + + case "${1:-}" in + install) + install + ;; + start) + start_services + ;; + stop) + stop_services + ;; + upgrade) + upgrade + ;; + "") + ask_for_action + ;; + *) + echo -e "${RED}Unknown command: $1${NC}" + echo "" + echo "Usage: $0 [install|start|stop|upgrade]" + echo "" + echo "Commands:" + echo " install - Install Whisper Money" + echo " start - Start Docker services" + echo " stop - Stop Docker services" + echo " upgrade - Upgrade Whisper Money" + echo "" + echo "If no command is provided, an interactive menu will be shown." + exit 1 + ;; + esac +} + +# Run main function +main "$@"