From 1e27f316730725a182055f27529f843e2c6c8b29 Mon Sep 17 00:00:00 2001 From: jekkos Date: Mon, 19 Oct 2026 19:27:53 +0200 Subject: [PATCH] feat(install): add one-line Ubuntu installer (#4550) * feat(install): add one-line Ubuntu installer with CI test and docs - Add scripts/install-ubuntu.sh: installs Apache/MariaDB/PHP, downloads the latest stable release, configures the DB + Apache, and optionally sets up Let's Encrypt SSL (interactive or via env vars). - Add .github/workflows/install-script-test.yml: runs the installer in a fresh Ubuntu container and sanity-checks the result. - Document the installer in INSTALL.md. - Harden the script: identifier-safe DB_NAME, quote/pipe rejection in passwords, correct 'Candidate: (none)' PPA detection, keep unix_socket root auth when no root password is set, select the .zip release asset, and use the SSL domain as the public hostname for allowedHostnames. --- .github/workflows/install-script-test.yml | 140 +++++++ docs/INSTALL.md | 76 +++- tools/scripts/install-ubuntu.sh | 480 ++++++++++++++++++++++ 3 files changed, 694 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/install-script-test.yml create mode 100644 tools/scripts/install-ubuntu.sh diff --git a/.github/workflows/install-script-test.yml b/.github/workflows/install-script-test.yml new file mode 100644 index 000000000..f0114da7b --- /dev/null +++ b/.github/workflows/install-script-test.yml @@ -0,0 +1,140 @@ +name: Install Script Test + +on: + push: + paths: + - 'tools/scripts/install-ubuntu.sh' + - '.github/workflows/install-script-test.yml' + pull_request: + paths: + - 'tools/scripts/install-ubuntu.sh' + - '.github/workflows/install-script-test.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + install-test: + name: Test Install Script (${{ matrix.scenario }}) + runs-on: ubuntu-22.04 + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + include: + - scenario: default + db_pass: '' + - scenario: custom-password + db_pass: 'TestPass123!' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Make install script executable + run: chmod +x tools/scripts/install-ubuntu.sh + + - name: Run install script + env: + DB_PASS: ${{ matrix.db_pass }} + run: | + set -o pipefail + echo "Running install script with scenario: ${{ matrix.scenario }}" + sudo -E bash tools/scripts/install-ubuntu.sh 2>&1 | tee install-output.log + echo "Install completed successfully" + + - name: Wait for services to stabilize + run: sleep 10 + + - name: Verify Apache is running + run: | + echo "Checking Apache status..." + sudo systemctl status apache2 --no-pager + sudo systemctl is-active apache2 + + - name: Verify MariaDB is running + run: | + echo "Checking MariaDB status..." + sudo systemctl status mariadb --no-pager + sudo systemctl is-active mariadb + + - name: Verify Apache HTTP response + run: | + echo "Testing HTTP response on port 80..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/) + echo "HTTP Response Code: $HTTP_CODE" + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then + echo "Apache is responding correctly" + elif [ "$HTTP_CODE" = "500" ]; then + echo "HTTP 500 - Application error. Checking .env configuration..." + # Never print secret values (password, encryption key) to the log; + # show only non-secret keys and confirm the sensitive ones are present. + sudo cat /var/www/ospos/.env 2>/dev/null | grep -E "database\.default\.(hostname|database|username)|CI_ENVIRONMENT" | head -10 + sudo grep -q '^encryption\.key=.' /var/www/ospos/.env 2>/dev/null && echo "encryption.key is set" || echo "WARNING: encryption.key is NOT set" + sudo cat /var/www/ospos/writable/logs/*.log 2>/dev/null | tail -20 || true + curl -s -L http://localhost/ | head -50 + exit 1 + else + echo "Unexpected HTTP code: $HTTP_CODE" + exit 1 + fi + + - name: Verify OSPOS login page + run: | + echo "Checking OSPOS login page..." + # Follow redirects (-L): the root path may 302 to the login route, so + # fetch the final page to actually see the login form. + curl -s -L http://localhost/ | grep -qi "login\|password\|username" && echo "Login page content found" || { + echo "Login page verification failed" + curl -s -L http://localhost/ | head -50 + exit 1 + } + + - name: Verify database exists + env: + DB_PASS: ${{ matrix.db_pass != '' && matrix.db_pass || '' }} + run: | + echo "Verifying database..." + + # Extract the generated password from install output if using default. + # The installer prints " Password: " (the first such line is the DB + # password; a later one is the app login password), so anchor on it and take + # the first match. + if [ -z "${{ matrix.db_pass }}" ]; then + GENERATED_PASS=$(grep -oP '^\s*Password: \K[^\s]+' install-output.log | head -n 1 || true) + if [ -n "$GENERATED_PASS" ]; then + DB_PASS="$GENERATED_PASS" + fi + fi + + # Check database exists + sudo mysql -u root -e "SHOW DATABASES LIKE 'ospos';" | grep -q ospos && echo "Database 'ospos' exists" || { + echo "Database 'ospos' not found" + sudo mysql -u root -e "SHOW DATABASES;" + exit 1 + } + + # Check tables exist (use the app account so the scenario actually proves + # `ospos` can authenticate with the configured DB_PASS, not just root) + TABLE_COUNT=$(sudo mysql -u ospos -p"$DB_PASS" ospos -e "SHOW TABLES;" | wc -l) + echo "Found $TABLE_COUNT tables in database" + if [ "$TABLE_COUNT" -gt 5 ]; then + echo "Database tables verified" + else + echo "Not enough tables found" + exit 1 + fi + + - name: Upload install log + uses: actions/upload-artifact@v4 + if: always() + with: + name: install-log-${{ matrix.scenario }} + path: install-output.log + retention-days: 7 \ No newline at end of file diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 3ed377024..856ede37f 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -112,5 +112,77 @@ Do **not** use below command on live deployments unless you want to tear everyth ## Cloud install -If you choose DigitalOcean: -[Through this link](https://m.do.co/c/ac38c262507b), you will get a [**free $100, 60-day credit**](https://m.do.co/c/ac38c262507b). [Check the wiki](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) for further instructions on how to install the necessary components. +### Recommended: DigitalOcean + +Sign up through [our referral link](https://m.do.co/c/ac38c262507b) to get a [**$100, 60-day credit**](https://m.do.co/c/ac38c262507b). + +1. Create an Ubuntu 20.04+ or 22.04+ droplet +2. SSH into your server: `ssh root@` +3. Run the one-line installer: + ```bash + curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/tools/scripts/install-ubuntu.sh | sudo bash + ``` + + > A shorter URL (`curl -sSL https://opensourcepos.org/install | sudo bash`) is also provided once the `opensourcepos.org/install` redirect is configured. + +The installer will: +- Install Apache, MariaDB, PHP 8.2 and required extensions +- Download the **latest stable release** of OSPOS from GitHub +- Create a database with secure random password +- Configure OSPOS and Apache +- **Set up SSL/TLS certificates** (interactive prompt or environment variables) +- Display login credentials after completion + +**Interactive Mode (Recommended for first-time users):** + +When run without environment variables, the installer will prompt you: +1. Whether to configure SSL (recommended for production) +2. Your domain name (e.g., `pos.example.com`) +3. Your email for Let's Encrypt (for production SSL) + +The prompts require a real terminal (standard input attached to a tty), so download +the script first and run it directly — do **not** pipe it, or the installer detects a +non-interactive session and skips the prompts: + +```bash +# 1. Download the installer +curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/tools/scripts/install-ubuntu.sh -o install-ospos.sh +# 2. Run it interactively (standard input stays attached to your terminal) +sudo bash install-ospos.sh +# Script will ask: +# - Configure SSL? (y/n) +# - Domain name: pos.example.com +# - Email for Let's Encrypt: admin@example.com +``` + +**Non-Interactive Mode (for automation):** + +```bash +# Development (no SSL) +curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/tools/scripts/install-ubuntu.sh | APACHE_SERVER_NAME=localhost sudo -E bash + +# Production with Let's Encrypt SSL +curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/tools/scripts/install-ubuntu.sh | APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash + +# Custom database password +curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/tools/scripts/install-ubuntu.sh | DB_PASS=securepassword APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash +``` + +**Environment variables:** +- `DB_HOST` - Database host (default: localhost) +- `DB_NAME` - Database name (default: ospos) +- `DB_USER` - Database user (default: ospos) +- `DB_PASS` - Database password (default: auto-generated) +- `MYSQL_ROOT_PASS` - MariaDB root password (default: empty/no password) +- `OSPOS_DIR` - Installation directory (default: /var/www/ospos) +- `OSPOS_VERSION` - OSPOS version to install (default: latest stable release) +- `PHP_VERSION` - PHP version (default: 8.2) +- `APACHE_SERVER_NAME` - Server hostname (default: localhost, or set interactively) +- `SSL_EMAIL` - Email for Let's Encrypt. Enables production SSL with auto-renewal **only when a public hostname is also provided** (via `SSL_DOMAIN` or a non-local `APACHE_SERVER_NAME`); `SSL_EMAIL` alone has no effect and the installer falls back to plain HTTP. +- `SSL_DOMAIN` - Domain for the SSL certificate. With `SSL_EMAIL` it uses Let's Encrypt; on its own it generates a self-signed certificate. Also serves as the installer's hostname when `APACHE_SERVER_NAME` is not set. + +> **Testing:** This installer is tested with each commit via our CI workflow. A fresh Ubuntu container is spawned, the script runs to completion, and basic sanity checks verify the installation. For production deployments, we recommend testing on a staging server first. If you encounter issues, please [open an issue](https://github.com/opensourcepos/opensourcepos/issues/new?template=bug_report.yml) with your server version and error output. + +For other cloud providers or manual installation, see the [detailed installation guide](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) in the wiki. + +**Important:** Change the default password after first login! diff --git a/tools/scripts/install-ubuntu.sh b/tools/scripts/install-ubuntu.sh new file mode 100644 index 000000000..ff92c2891 --- /dev/null +++ b/tools/scripts/install-ubuntu.sh @@ -0,0 +1,480 @@ +#!/bin/bash + +set -e + +COLOR_RED='\033[0;31m' +COLOR_GREEN='\033[0;32m' +COLOR_YELLOW='\033[1;33m' +COLOR_BLUE='\033[0;34m' +COLOR_RESET='\033[0m' + +echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}" +echo -e "${COLOR_BLUE}║ Open Source Point of Sale - Ubuntu Installer ║${COLOR_RESET}" +echo -e "${COLOR_BLUE}║ Version 3.4+ ║${COLOR_RESET}" +echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}" +echo "" + +if [ "$EUID" -ne 0 ]; then + echo -e "${COLOR_RED}Please run this script as root or with sudo${COLOR_RESET}" + exit 1 +fi + +export DEBIAN_FRONTEND=noninteractive + +DB_HOST="${DB_HOST:-localhost}" +DB_NAME="${DB_NAME:-ospos}" +DB_USER="${DB_USER:-ospos}" +DB_PASS="${DB_PASS:-$(openssl rand -base64 24)}" +OSPOS_DIR="${OSPOS_DIR:-/var/www/ospos}" +OSPOS_VERSION="${OSPOS_VERSION:-}" +PHP_VERSION="${PHP_VERSION:-8.2}" +APACHE_SERVER_NAME="${APACHE_SERVER_NAME:-}" +SSL_EMAIL="${SSL_EMAIL:-}" +SSL_DOMAIN="${SSL_DOMAIN:-}" +MYSQL_ROOT_PASS="${MYSQL_ROOT_PASS:-}" + +# Validate that a variable matches the given pattern, exiting with an error otherwise. +validate_var() { + local var_name="$1" + local var_value="$2" + local pattern="$3" + local hint="$4" + if [[ ! "$var_value" =~ $pattern ]]; then + echo -e "${COLOR_RED}Error: ${var_name} is invalid. ${hint}${COLOR_RESET}" + exit 1 + fi +} + +# DB_NAME is used as an unquoted SQL identifier (CREATE DATABASE / GRANT ... ON db.*), +# so restrict it to identifier-safe characters (no dots or hyphens). +validate_var "DB_NAME" "$DB_NAME" '^[a-zA-Z0-9_]+$' "Use only letters, numbers, and underscores." + +# DB_USER and DB_HOST are quoted in SQL, so dots and hyphens (hostnames/IPs) are safe. +validate_var "DB_USER" "$DB_USER" '^[a-zA-Z0-9_.\-]+$' "Use only letters, numbers, underscores, dots, and hyphens." +validate_var "DB_HOST" "$DB_HOST" '^[a-zA-Z0-9_.\-]+$' "Use only letters, numbers, underscores, dots, and hyphens." + +# DB_PASS and MYSQL_ROOT_PASS are embedded inside single-quoted SQL, so a single quote +# would break the statement. DB_PASS also passes through a sed replacement that uses '|' +# as its delimiter, so reject that too. Backslashes are escape characters in MySQL +# string literals, and double quotes conflict with the MariaDB config-file syntax, so +# reject both in MYSQL_ROOT_PASS. +if [[ "$DB_PASS" == *"'"* || "$DB_PASS" == *'|'* ]]; then + echo -e "${COLOR_RED}Error: DB_PASS must not contain single quotes (') or pipe characters (|).${COLOR_RESET}" + exit 1 +fi +if [[ "$MYSQL_ROOT_PASS" == *"'"* || "$MYSQL_ROOT_PASS" == *'"'* || "$MYSQL_ROOT_PASS" == *'\\'* ]]; then + echo -e "${COLOR_RED}Error: MYSQL_ROOT_PASS must not contain quotes (', \") or backslashes (\\).${COLOR_RESET}" + exit 1 +fi + +# Check if running interactively +INTERACTIVE=false +if [ -t 0 ]; then + INTERACTIVE=true +fi + +echo -e "${COLOR_YELLOW}Configuration:${COLOR_RESET}" +echo -e " Database Name: ${DB_NAME}" +echo -e " Database User: ${DB_USER}" +echo -e " Database Host: ${DB_HOST}" +echo -e " Install Directory: ${OSPOS_DIR}" +echo -e " PHP Version: ${PHP_VERSION}" +if [ -n "$OSPOS_VERSION" ]; then + echo -e " OSPOS Version: ${OSPOS_VERSION}" +else + echo -e " OSPOS Version: latest" +fi +if [ -n "$APACHE_SERVER_NAME" ]; then + echo -e " Server Name: ${APACHE_SERVER_NAME}" +fi +echo "" + +if [ -d "$OSPOS_DIR" ]; then + echo -e "${COLOR_RED}Installation directory $OSPOS_DIR already exists${COLOR_RESET}" + echo -e "${COLOR_YELLOW}Remove it or set OSPOS_DIR environment variable${COLOR_RESET}" + exit 1 +fi + +echo -e "${COLOR_GREEN}[1/9] Updating system packages...${COLOR_RESET}" +apt-get update -qq + +echo -e "${COLOR_GREEN}[2/9] Installing Apache, PHP, and dependencies...${COLOR_RESET}" +# Add the ondrej/php PPA only when the requested PHP package has no real candidate in +# the default repos. "Candidate: (none)" means the package is unavailable, so treat it +# the same as a missing package rather than a satisfied one. +PHP_CANDIDATE=$(apt-cache policy "php${PHP_VERSION}" 2>/dev/null | awk '/^[[:space:]]*Candidate:/{print $2; exit}') +if [ -z "$PHP_CANDIDATE" ] || [ "$PHP_CANDIDATE" = "(none)" ]; then + echo -e "${COLOR_YELLOW}PHP ${PHP_VERSION} not in default repos, adding ondrej/php PPA...${COLOR_RESET}" + apt-get install -y -qq software-properties-common + add-apt-repository -y ppa:ondrej/php + apt-get update -qq +fi + +apt-get install -y -qq \ + apache2 \ + mariadb-server \ + mariadb-client \ + php${PHP_VERSION} \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-bcmath \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-zip \ + git \ + curl \ + unzip \ + openssl + +echo -e "${COLOR_GREEN}[3/9] Starting MariaDB...${COLOR_RESET}" +systemctl start mariadb +systemctl enable mariadb + +# When a MariaDB root password is supplied, pass it through a private defaults file +# (mode 0600) for authenticated connections, so the credential never appears on the +# command line / in `ps` (CWE-214). The file is removed when the script exits. +ROOT_CNF="" +ROOT_SQL="" +if [ -n "$MYSQL_ROOT_PASS" ]; then + ROOT_CNF=$(mktemp "${TMPDIR:-/tmp}/ospos-root-cnf.XXXXXX") + chmod 600 "$ROOT_CNF" + printf '[client]\nuser=root\npassword=%s\n' "$MYSQL_ROOT_PASS" > "$ROOT_CNF" +fi +cleanup_temp_files() { + [ -n "${ROOT_CNF:-}" ] && rm -f "$ROOT_CNF" || true + [ -n "${ROOT_SQL:-}" ] && rm -f "$ROOT_SQL" || true +} +trap cleanup_temp_files EXIT + +# mysql_root: connect as root, using the private defaults file when a root password +# was supplied (otherwise rely on the default unix_socket auth for the OS root user). +mysql_root() { + if [ -n "$ROOT_CNF" ]; then + mysql --defaults-extra-file="$ROOT_CNF" "$@" + else + mysql -u root "$@" + fi +} + +if [ -n "$MYSQL_ROOT_PASS" ]; then + echo -e "${COLOR_BLUE}Setting MariaDB root password...${COLOR_RESET}" + # Set the password via unix_socket auth (no password is set yet) and flush in the + # same session: once a password is set, unix_socket auth for root is dropped, so a + # separate passwordless `mysql` call would fail. The password lives only in this + # 0600 temp file, never on the process command line. + ROOT_SQL=$(mktemp "${TMPDIR:-/tmp}/ospos-root-sql.XXXXXX") + chmod 600 "$ROOT_SQL" + printf "ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';\nFLUSH PRIVILEGES;\n" "$MYSQL_ROOT_PASS" > "$ROOT_SQL" + mysql -u root < "$ROOT_SQL" +else + # No root password supplied: keep the default unix_socket authentication for root. + echo -e "${COLOR_BLUE}Keeping MariaDB root on unix_socket authentication...${COLOR_RESET}" +fi + +echo -e "${COLOR_GREEN}[4/9] Creating database and user...${COLOR_RESET}" +# This installer provisions a local MariaDB and OSPOS runs on the same host, so the +# account's client host is 'localhost' (the client host, not a remote server name). +mysql_root </dev/null + +if [ -f "composer.json" ]; then + echo -e "${COLOR_BLUE}Installing dependencies...${COLOR_RESET}" + composer install --no-dev --optimize-autoloader --no-interaction --quiet 2>/dev/null +fi + +echo -e "${COLOR_GREEN}[7/9] Configuring OSPOS...${COLOR_RESET}" +if [ -f ".env.example" ]; then + cp .env.example .env +fi + +if [ -f ".env" ]; then + # Escape special characters in password for sed + ESCAPED_DB_PASS=$(printf '%s\n' "$DB_PASS" | sed 's/[&/\]/\\&/g') + + sed -i "s|database\.default\.hostname='localhost'|database.default.hostname='${DB_HOST}'|" .env + sed -i "s|database\.default\.database='ospos'|database.default.database='${DB_NAME}'|" .env + sed -i "s|database\.default\.username='admin'|database.default.username='${DB_USER}'|" .env + sed -i "s|database\.default\.password='pointofsale'|database.default.password='${ESCAPED_DB_PASS}'|" .env + sed -i "s|CI_ENVIRONMENT=development|CI_ENVIRONMENT=production|" .env + # Production requires an allowed-hostname whitelist (the app refuses to boot + # without one). Default to localhost (the installer's default server name); + # real installs with a domain should update this to their hostname(s). + sed -i "s|app.allowedHostnames=''|app.allowedHostnames='localhost'|" .env + + if grep -q "encryption\.key=''" .env; then + ENCRYPTION_KEY=$(openssl rand -base64 32) + ESCAPED_KEY=$(printf '%s\n' "$ENCRYPTION_KEY" | sed 's/[&/\]/\\&/g') + sed -i "s|encryption\.key=''|encryption.key='${ESCAPED_KEY}'|" .env + fi +fi + +echo -e "${COLOR_GREEN}[8/9] Running database migrations...${COLOR_RESET}" +# The OSPOS release ships as a pre-built dist that omits the `spark` CLI entry point +# (the framework system dir, app/Config/Paths.php and the App migrations are present), +# so restore the standard CodeIgniter 4 `spark` bootstrap, then apply the schema with +# the framework runner now that .env credentials point at the new database. +if [ ! -f spark ]; then + cat > spark <<'SPARK_EOF' +#!/usr/bin/env php +systemDirectory . '/Boot.php'; + +exit(Boot::bootSpark($paths)); +SPARK_EOF + chmod +x spark +fi +php spark migrate + +# Interactive SSL configuration +if $INTERACTIVE && [ -z "$SSL_EMAIL" ] && [ -z "$APACHE_SERVER_NAME" ]; then + echo "" + echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}" + echo -e "${COLOR_BLUE}║ SSL/TLS Configuration ║${COLOR_RESET}" + echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}" + echo "" + echo -e "${COLOR_YELLOW}SSL provides secure HTTPS access to your OSPOS installation.${COLOR_RESET}" + echo -e "${COLOR_YELLOW}For production, we recommend Let's Encrypt (free SSL certificate).${COLOR_RESET}" + echo "" + + read -p "Configure SSL? (y/n) [n]: " CONFIGURE_SSL + CONFIGURE_SSL=${CONFIGURE_SSL:-n} + + if [[ "$CONFIGURE_SSL" =~ ^[Yy]$ ]]; then + read -p "Enter your domain name (e.g., pos.example.com): " SSL_DOMAIN + SSL_DOMAIN=${SSL_DOMAIN:-localhost} + APACHE_SERVER_NAME=$SSL_DOMAIN + + read -p "Enter your email for Let's Encrypt notifications: " SSL_EMAIL + + if [ -z "$SSL_EMAIL" ]; then + echo -e "${COLOR_YELLOW}No email provided. Using self-signed certificate (not recommended for production).${COLOR_RESET}" + SSL_TYPE="self-signed" + else + SSL_TYPE="letsencrypt" + fi + else + APACHE_SERVER_NAME="localhost" + SSL_TYPE="none" + fi +fi + +# Default the server name: prefer the SSL domain (the public hostname) when it is set, +# otherwise fall back to localhost. +if [ -z "$APACHE_SERVER_NAME" ]; then + APACHE_SERVER_NAME="${SSL_DOMAIN:-localhost}" +fi + +# If SSL_EMAIL is set without SSL_DOMAIN, derive the SSL domain from the server name +if [ -n "$SSL_EMAIL" ] && [ -z "$SSL_DOMAIN" ] && [ "$APACHE_SERVER_NAME" != "localhost" ]; then + SSL_DOMAIN="$APACHE_SERVER_NAME" +fi + +echo -e "${COLOR_GREEN}[9/9] Configuring Apache...${COLOR_RESET}" +cat > /etc/apache2/sites-available/ospos.conf < + ServerName ${APACHE_SERVER_NAME} + DocumentRoot ${OSPOS_DIR}/public + + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog \${APACHE_LOG_DIR}/ospos_error.log + CustomLog \${APACHE_LOG_DIR}/ospos_access.log combined + +EOF + +a2enmod rewrite +a2dissite 000-default.conf +a2ensite ospos.conf + +chown -R www-data:www-data "${OSPOS_DIR}" +chmod -R 750 "${OSPOS_DIR}/writable" + +systemctl restart apache2 +systemctl enable apache2 + +# Configure SSL if requested +if [ -n "$SSL_EMAIL" ] && [ -n "$SSL_DOMAIN" ]; then + # Let's Encrypt SSL + echo -e "${COLOR_BLUE}Installing Certbot for Let's Encrypt...${COLOR_RESET}" + apt-get install -y -qq certbot python3-certbot-apache + + echo -e "${COLOR_BLUE}Obtaining SSL certificate for ${SSL_DOMAIN}...${COLOR_RESET}" + certbot --apache -d ${SSL_DOMAIN} --non-interactive --agree-tos --email ${SSL_EMAIL} --redirect + + echo -e "${COLOR_BLUE}Setting up auto-renewal...${COLOR_RESET}" + systemctl enable certbot.timer + systemctl start certbot.timer + + PROTOCOL="https" + FINAL_URL="https://${SSL_DOMAIN}/" +elif [ -n "$SSL_DOMAIN" ]; then + # Self-signed SSL + echo -e "${COLOR_BLUE}Generating self-signed SSL certificate...${COLOR_RESET}" + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/ospos-selfsigned.key \ + -out /etc/ssl/certs/ospos-selfsigned.crt \ + -subj "/C=US/ST=State/L=City/O=Organization/CN=${SSL_DOMAIN}" 2>/dev/null + + cat > /etc/apache2/sites-available/ospos-ssl.conf < + ServerName ${SSL_DOMAIN} + DocumentRoot ${OSPOS_DIR}/public + + SSLEngine on + SSLCertificateFile /etc/ssl/certs/ospos-selfsigned.crt + SSLCertificateKeyFile /etc/ssl/private/ospos-selfsigned.key + + + Options -Indexes +FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog \${APACHE_LOG_DIR}/ospos_ssl_error.log + CustomLog \${APACHE_LOG_DIR}/ospos_ssl_access.log combined + +EOF + + a2enmod ssl + a2ensite ospos-ssl.conf + + cat > /etc/apache2/sites-available/ospos.conf < + ServerName ${SSL_DOMAIN} + Redirect permanent / https://${SSL_DOMAIN}/ + +EOF + + a2dissite ospos.conf + a2ensite ospos.conf + + PROTOCOL="https" + FINAL_URL="https://${SSL_DOMAIN}/" + + echo -e "${COLOR_YELLOW}Note: Your browser will show a security warning for self-signed${COLOR_RESET}" + echo -e "${COLOR_YELLOW} certificates. For production, re-run with an email for Let's Encrypt.${COLOR_RESET}" +else + PROTOCOL="http" + FINAL_URL="http://${APACHE_SERVER_NAME}/" +fi + +systemctl restart apache2 + +# Configure allowed hostnames: use the public hostname (SSL domain when present) so +# Host-header validation accepts the domain the site is actually served at. +if [ -f "${OSPOS_DIR}/.env" ]; then + ALLOWED_HOST="${SSL_DOMAIN:-$APACHE_SERVER_NAME}" + # The shipped .env has no app.allowedHostnames line, so a plain sed is a silent + # no-op and production then 500s on first load; append it if it's missing. + if grep -Eq "^[[:space:]]*app\.allowedHostnames[[:space:]]*=" "${OSPOS_DIR}/.env"; then + sed -i -E "s|^[[:space:]]*app\.allowedHostnames[[:space:]]*=.*|app.allowedHostnames='${ALLOWED_HOST}'|" "${OSPOS_DIR}/.env" + else + printf "app.allowedHostnames='%s'\n" "$ALLOWED_HOST" >> "${OSPOS_DIR}/.env" + fi +fi + +echo "" +echo -e "${COLOR_GREEN}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}" +echo -e "${COLOR_GREEN}║ Installation Complete! ║${COLOR_RESET}" +echo -e "${COLOR_GREEN}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}" +echo "" +echo -e "${COLOR_YELLOW}Database Credentials:${COLOR_RESET}" +echo -e " Database: ${DB_NAME}" +echo -e " Username: ${DB_USER}" +echo -e " Password: ${DB_PASS}" +echo "" +echo -e "${COLOR_YELLOW}Login Credentials:${COLOR_RESET}" +echo -e " URL: ${FINAL_URL}" +if [ -n "$SSL_EMAIL" ]; then + echo -e " SSL: Let's Encrypt (auto-renewal enabled)" +elif [ -n "$SSL_DOMAIN" ]; then + echo -e " SSL: Self-signed certificate" +else + echo -e " SSL: Not configured (HTTP only)" +fi +echo -e " Username: admin" +echo -e " Password: pointofsale" +echo "" +echo -e "${COLOR_RED}IMPORTANT: Change the default password after first login!${COLOR_RESET}" +echo "" +echo -e "${COLOR_BLUE}Configuration file: ${OSPOS_DIR}/.env${COLOR_RESET}" +echo "" \ No newline at end of file