#!/bin/bash # PDF Sort and Merge Application - Ubuntu Installation Script # This script installs and configures the Flask web application for PDF processing set -e # Exit on any error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Check if running as root if [[ $EUID -eq 0 ]]; then log_error "This script should not be run as root. Please run as a regular user with sudo privileges." exit 1 fi # Check Ubuntu version UBUNTU_VERSION=$(lsb_release -rs) log_info "Detected Ubuntu version: $UBUNTU_VERSION" # Update system log_info "Updating system packages..." sudo apt update && sudo apt upgrade -y # Install Python 3.9+ and pip log_info "Installing Python and pip..." sudo apt install -y python3 python3-pip python3-venv python3-dev # Install system dependencies for PDF processing log_info "Installing system dependencies..." sudo apt install -y build-essential libpoppler-cpp-dev pkg-config # Install Node.js and npm (for potential frontend assets) log_info "Installing Node.js and npm..." curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - sudo apt install -y nodejs # Create application directory APP_DIR="$HOME/pdf-sort-merge" log_info "Creating application directory: $APP_DIR" mkdir -p "$APP_DIR" cd "$APP_DIR" # Clone or download the application REPO_URL="https://gitlab.com/trbc-dev/eoy-pdf-sortandmerge.git" log_info "Cloning application repository..." if command -v git &> /dev/null; then sudo apt install -y git if [[ -d ".git" ]]; then log_info "Repository already exists, pulling latest changes..." git pull origin web-app git checkout web-app else log_info "Cloning fresh repository..." git clone "$REPO_URL" . git checkout web-app fi else log_warning "Git not available. Please manually download and extract the application files to $APP_DIR" exit 1 fi # Set permissions for static files (nginx needs to read them) log_info "Setting permissions for static files..." chmod -R 755 static/ find static/ -type f -exec chmod 644 {} \; # Ensure nginx can access static files - try chown, fallback to group permissions if sudo chown -R www-data:www-data static/ 2>/dev/null; then log_success "Static files ownership set to www-data" else log_warning "Could not change ownership to www-data, using group permissions instead" # Add user to www-data group for nginx access sudo usermod -a -G www-data $USER 2>/dev/null || true sudo chmod -R 755 static/ find static/ -type f -exec sudo chmod 644 {} \; log_info "Set group permissions for nginx access" fi # Create Python virtual environment log_info "Creating Python virtual environment..." python3 -m venv venv source venv/bin/activate # Install Python dependencies log_info "Installing Python dependencies..." pip install --upgrade pip pip install -r requirements.txt # Try to install PyMuPDF (optional for PDF processing) log_info "Attempting to install PyMuPDF for PDF processing..." if pip install PyMuPDF==1.23.7; then log_success "PyMuPDF installed successfully" else log_warning "PyMuPDF installation failed - PDF processing will not be available" log_warning "You can try installing it manually later with: pip install PyMuPDF" fi # Create .env file with Azure AD configuration log_info "Configuring Azure AD authentication..." echo "" echo "==================================================================" echo "AZURE AD CONFIGURATION REQUIRED" echo "==================================================================" echo "You need to provide your Azure AD app registration details." echo "If you don't have them, follow these steps:" echo "1. Go to https://portal.azure.com" echo "2. Navigate to Azure Active Directory > App registrations" echo "3. Create a new registration or use existing one" echo "4. Note the Application (client) ID and Directory (tenant) ID" echo "5. Create a client secret under Certificates & secrets" echo "6. Add redirect URI: http://your-domain-name/auth/callback (will be configured automatically)" echo "==================================================================" echo "" # Prompt for Azure AD credentials read -p "Enter Azure AD Tenant ID (Directory ID): " AZURE_TENANT_ID read -p "Enter Azure AD Client ID (Application ID): " AZURE_CLIENT_ID read -s -p "Enter Azure AD Client Secret: " AZURE_CLIENT_SECRET echo "" read -p "Enter DNS domain name (e.g., eoypdf.trbc.org) [eoypdf.trbc.org]: " DNS_DOMAIN DNS_DOMAIN=${DNS_DOMAIN:-eoypdf.trbc.org} read -p "Enter server IP/domain (for redirect URI) [$DNS_DOMAIN]: " SERVER_HOST SERVER_HOST=${SERVER_HOST:-$DNS_DOMAIN} # Determine if we should use HTTPS USE_HTTPS=false GENERATE_SELF_SIGNED=false if [[ "$DNS_DOMAIN" != "localhost" && "$DNS_DOMAIN" != "127.0.0.1" ]]; then read -p "Do you have SSL certificates for $DNS_DOMAIN? (y/n) [n]: " HAS_SSL if [[ "${HAS_SSL,,}" == "y" || "${HAS_SSL,,}" == "yes" ]]; then USE_HTTPS=true else read -p "Would you like to generate self-signed SSL certificates? (y/n) [y]: " GEN_SELF GEN_SELF=${GEN_SELF:-y} if [[ "${GEN_SELF,,}" == "y" || "${GEN_SELF,,}" == "yes" ]]; then USE_HTTPS=true GENERATE_SELF_SIGNED=true fi fi fi # Create .env file with real values cat > .env << EOF # Microsoft Entra (Azure AD) Configuration # Get these values from your Azure AD app registration in the Azure portal # Azure AD Tenant ID (Directory ID) AZURE_TENANT_ID=$AZURE_TENANT_ID # Azure AD Client ID (Application ID) AZURE_CLIENT_ID=$AZURE_CLIENT_ID # Azure AD Client Secret AZURE_CLIENT_SECRET=$AZURE_CLIENT_SECRET # Azure AD Authority URL (usually https://login.microsoftonline.com/{tenant_id}) AZURE_AUTHORITY=https://login.microsoftonline.com/$AZURE_TENANT_ID # Redirect URI (must match what's configured in Azure AD app registration) AZURE_REDIRECT_URI=http${USE_HTTPS:+s}://$SERVER_HOST/auth/callback # Scopes to request (openid, profile, email are common) AZURE_SCOPES=openid profile email # Flask Configuration SECRET_KEY=$(openssl rand -hex 32) EOF log_success "Azure AD configuration saved to .env file" # Generate self-signed SSL certificates if requested if [[ "$GENERATE_SELF_SIGNED" == "true" ]]; then log_info "Generating self-signed SSL certificates..." sudo mkdir -p /etc/ssl/certs /etc/ssl/private # Generate self-signed certificate sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/ssl/private/pdf-sort-merge.key \ -out /etc/ssl/certs/pdf-sort-merge.crt \ -subj "/C=US/ST=State/L=City/O=Organization/CN=$DNS_DOMAIN" \ -addext "subjectAltName=DNS:$DNS_DOMAIN" log_success "Self-signed SSL certificates generated" fi # Install and configure nginx log_info "Installing and configuring nginx..." sudo apt install -y nginx # Create nginx configuration NGINX_CONF="/etc/nginx/sites-available/pdf-sort-merge" if [[ "$USE_HTTPS" == "true" ]]; then # HTTPS configuration with SSL sudo tee "$NGINX_CONF" > /dev/null << EOF server { listen 80; server_name $DNS_DOMAIN; return 301 https://\$server_name\$request_uri; } server { listen 443 ssl http2; server_name $DNS_DOMAIN; ssl_certificate /etc/ssl/certs/pdf-sort-merge.crt; ssl_certificate_key /etc/ssl/private/pdf-sort-merge.key; # SSL security settings ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; ssl_prefer_server_ciphers off; # Large file upload settings client_max_body_size 100G; client_body_timeout 300s; client_header_timeout 60s; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Proxy to Flask app location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # WebSocket support (if needed) proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; # Timeout settings proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; # Disable buffering for large uploads proxy_buffering off; proxy_request_buffering off; } # Static files location /static/ { alias $APP_DIR/static/; expires 1y; add_header Cache-Control "public, immutable"; } } EOF else # HTTP-only configuration sudo tee "$NGINX_CONF" > /dev/null << EOF server { listen 80; server_name $DNS_DOMAIN; # Large file upload settings client_max_body_size 100G; client_body_timeout 300s; client_header_timeout 60s; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; # Proxy to Flask app location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; # WebSocket support (if needed) proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; # Timeout settings proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; # Disable buffering for large uploads proxy_buffering off; proxy_request_buffering off; } # Static files location /static/ { alias $APP_DIR/static/; expires 1y; add_header Cache-Control "public, immutable"; } } EOF fi # Enable the site sudo ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/ sudo rm -f /etc/nginx/sites-enabled/default # Test nginx configuration log_info "Testing nginx configuration..." if sudo nginx -t; then log_success "Nginx configuration is valid" else log_error "Nginx configuration has errors" exit 1 fi # Restart nginx sudo systemctl restart nginx sudo systemctl enable nginx log_success "Nginx configured to serve $DNS_DOMAIN on port 80" # Create systemd service file log_info "Creating systemd service..." SERVICE_FILE="/etc/systemd/system/pdf-sort-merge.service" sudo tee "$SERVICE_FILE" > /dev/null << EOF [Unit] Description=PDF Sort and Merge Flask Application After=network.target [Service] Type=simple User=$USER WorkingDirectory=$APP_DIR Environment=PATH=$APP_DIR/venv/bin Environment=PYTHONPATH=$APP_DIR ExecStart=$APP_DIR/venv/bin/python $APP_DIR/app.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF # Set proper permissions sudo chmod 644 "$SERVICE_FILE" # Create log directory log_info "Creating log directory..." sudo mkdir -p /var/log/pdf-sort-merge sudo chown $USER:$USER /var/log/pdf-sort-merge # Create application data directories log_info "Creating application data directories..." mkdir -p results static templates # Create Flask session directory for server-side sessions log_info "Creating Flask session directory..." sudo mkdir -p /tmp/flask_sessions sudo chown $USER:$USER /tmp/flask_sessions # Set up firewall (optional) log_info "Configuring firewall..." sudo ufw allow 80/tcp || log_warning "Failed to configure firewall for port 80 - you may need to do this manually" if [[ "$USE_HTTPS" == "true" ]]; then sudo ufw allow 443/tcp || log_warning "Failed to configure firewall for port 443 - you may need to do this manually" fi sudo ufw allow ssh/tcp || log_warning "Failed to configure firewall for SSH - you may need to do this manually" # Enable and start service log_info "Enabling and starting service..." sudo systemctl daemon-reload sudo systemctl enable pdf-sort-merge sudo systemctl start pdf-sort-merge # Check service status sleep 3 if sudo systemctl is-active --quiet pdf-sort-merge; then log_success "Service started successfully!" else log_error "Service failed to start. Check logs with: sudo journalctl -u pdf-sort-merge -f" fi # Create management script log_info "Creating management script..." cat > manage.sh << 'EOF' #!/bin/bash case "$1" in start) sudo systemctl start pdf-sort-merge echo "Service started" ;; stop) sudo systemctl stop pdf-sort-merge echo "Service stopped" ;; restart) sudo systemctl restart pdf-sort-merge echo "Service restarted" ;; status) sudo systemctl status pdf-sort-merge ;; logs) sudo journalctl -u pdf-sort-merge -f ;; update) cd /home/$USER/pdf-sort-merge source venv/bin/activate git pull pip install -r requirements.txt sudo systemctl restart pdf-sort-merge echo "Application updated" ;; *) echo "Usage: $0 {start|stop|restart|status|logs|update}" exit 1 ;; esac EOF chmod +x manage.sh # Final instructions log_success "Installation completed!" echo "" echo "==================================================================" echo "NEXT STEPS:" echo "1. Point your DNS for $DNS_DOMAIN to this server's IP address" echo "" if [[ "$GENERATE_SELF_SIGNED" == "true" ]]; then echo "2. IMPORTANT: Self-signed certificates are now active!" echo " - Browsers will show a security warning - click 'Advanced' and 'Proceed'" echo " - For production, consider getting a proper SSL certificate from Let's Encrypt" echo "" fi echo "3. Update your Azure AD app registration redirect URI to:" if [[ "$USE_HTTPS" == "true" ]]; then echo " https://$SERVER_HOST/auth/callback" else echo " http://$SERVER_HOST/auth/callback" fi echo "" echo "4. Access the application at:" if [[ "$USE_HTTPS" == "true" ]]; then echo " https://$DNS_DOMAIN" else echo " http://$DNS_DOMAIN" fi echo "" echo "MANAGEMENT COMMANDS:" echo " Start service: sudo systemctl start pdf-sort-merge" echo " Stop service: sudo systemctl stop pdf-sort-merge" echo " Restart nginx: sudo systemctl restart nginx" echo " Check status: sudo systemctl status pdf-sort-merge" echo " View logs: sudo journalctl -u pdf-sort-merge -f" echo " Update app: ./manage.sh update" echo "==================================================================" # Check if .env needs configuration if [[ -z "$AZURE_TENANT_ID" || -z "$AZURE_CLIENT_ID" || -z "$AZURE_CLIENT_SECRET" ]]; then log_warning "IMPORTANT: Azure AD configuration is incomplete. Please check your .env file!" fi