diff --git a/AGENTS.md b/AGENTS.md index f67e903..56a46ab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,5 +7,7 @@ - Run Pa11y for layout or functionality adjustments (skip for text-only edits). - Run Linkinator only when links change. - Whenever code changes touch workflows or contributor steps, update `README.md` or related docs to keep guidance accurate. +- Keep the Ubuntu installer turnkey: auto-detect Apache vs. Nginx and MySQL vs. MariaDB, seed the database by default, and ensure configuration uses the repository root even when run from another path. +- Provide containerized workflows with Docker Compose that support either MySQL or MariaDB without manual tweaks. - Use offline-friendly validation when the network blocks npm installs: `python tools/html_checks.py ` and `python tools/a11y_checks.py ` for each modified HTML document. Prefer the standard npm-based tools when available. - Document the exact validation commands you execute in PR summaries to streamline reviews. diff --git a/README.md b/README.md index 60ec930..30596d8 100644 --- a/README.md +++ b/README.md @@ -184,24 +184,55 @@ CREATE TABLE incident_types (id INT PRIMARY KEY, name VARCHAR(50)); ## 🚀 Installation -### 1. Clone the repo +### Option A: Automated Ubuntu setup + +Run the installer from the repository root (requires `sudo`): + +```bash +sudo ./install_ubuntu.sh +``` + +Installer options: + +- `--db [mysql|mariadb]` → choose the database server to install (default: auto-detects an existing install or uses `mysql`). +- `--db-name` → database name (default: `accidents`). +- `--db-user` / `--db-pass` → credentials that match `db/connect.php` defaults (`dbuser` / `dbpass`). +- `--web [apache|nginx]` → choose a web server (default: auto-detects an existing install or uses `apache`). +- `--skip-seed` → skip importing the bundled `seed.sql` file. + +The script configures the chosen web server to serve this repository and seeds the database with sample data. After completion, visit [http://localhost](http://localhost). + +### Option B: Docker Compose (local) + +Build and start the stack (defaults to MySQL on port 3306 and the app on 8080): + +```bash +docker compose up --build +``` + +Use `DB_IMAGE` to switch to MariaDB or change credentials/port exposure as needed: + +```bash +DB_IMAGE=mariadb:11 DB_NAME=accidents DB_USER=dbuser DB_PASS=dbpass docker compose up --build +``` + +Access the site at [http://localhost:8080](http://localhost:8080) and the database on `localhost:3306`. + +### Option C: Manual install + +#### 1. Clone the repo ```bash git clone https://github.com/ChadOhman/opensafetymap.git cd accident-reports ``` -### 2. Set up database +#### 2. Set up database - Import schema above into MySQL - Add reference data for categories, severity, incident types - Create initial admin user manually -### 3. Configure DB connection -Edit `db/connect.php`: -```php -$dsn = "mysql:host=localhost;dbname=accidents;charset=utf8mb4"; -$user = "your_mysql_user"; -$pass = "your_mysql_pass"; -``` +#### 3. Configure DB connection +Edit `db/connect.php` or set environment variables for your connection details (`DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS`). ### 4. Configure S3 - Update report submission API to upload photos to your S3 bucket diff --git a/db/connect.php b/db/connect.php index 4755ac8..4d9e37c 100644 --- a/db/connect.php +++ b/db/connect.php @@ -1,7 +1,10 @@ PDO::ERRMODE_EXCEPTION, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..85d9300 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.9" + +services: + app: + build: + context: . + dockerfile: docker/Dockerfile + ports: + - "8080:80" + environment: + DB_HOST: db + DB_NAME: ${DB_NAME:-accidents} + DB_USER: ${DB_USER:-dbuser} + DB_PASS: ${DB_PASS:-dbpass} + depends_on: + db: + condition: service_healthy + volumes: + - ./:/var/www/html + restart: unless-stopped + + db: + image: ${DB_IMAGE:-mysql:8.0} + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-example} + MYSQL_DATABASE: ${DB_NAME:-accidents} + MYSQL_USER: ${DB_USER:-dbuser} + MYSQL_PASSWORD: ${DB_PASS:-dbpass} + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + - ./seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h localhost -p$${MYSQL_ROOT_PASSWORD} --silent"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 10s + restart: unless-stopped + +volumes: + db_data: diff --git a/docker/Dockerfile b/docker/Dockerfile index 12e021d..ce7bf3b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,19 +1,11 @@ -# PHP + Apache setup FROM php:8.2-apache -# Install extensions -RUN docker-php-ext-install mysqli pdo pdo_mysql +RUN docker-php-ext-install mysqli pdo pdo_mysql \ + && a2enmod rewrite -# Enable Apache mod_rewrite -RUN a2enmod rewrite - -# Copy project files into container WORKDIR /var/www/html -COPY ./public /var/www/html/public -COPY ./api /var/www/html/api -COPY ./db /var/www/html/db +COPY . /var/www/html -# Set permissions RUN chown -R www-data:www-data /var/www/html EXPOSE 80 diff --git a/install_ubuntu.sh b/install_ubuntu.sh new file mode 100755 index 0000000..aebf366 --- /dev/null +++ b/install_ubuntu.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash +set -euo pipefail + +DB_ENGINE="auto" +DB_ENGINE_RESOLVED="" +DB_NAME="accidents" +DB_USER="dbuser" +DB_PASS="dbpass" +SKIP_SEED=0 +WEB_SERVER="auto" +WEB_SERVER_RESOLVED="" +APP_ROOT="$(cd "$(dirname "$0")" && pwd)" + +usage() { + cat <&2 + exit 1 + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --db) + DB_ENGINE="$2"; shift 2 ;; + --db-name) + DB_NAME="$2"; shift 2 ;; + --db-user) + DB_USER="$2"; shift 2 ;; + --db-pass) + DB_PASS="$2"; shift 2 ;; + --web) + WEB_SERVER="$2"; shift 2 ;; + --skip-seed) + SKIP_SEED=1; shift ;; + -h|--help) + usage; exit 0 ;; + *) + echo "Unknown option: $1" >&2 + usage + exit 1 ;; + esac + done + + if [[ "$DB_ENGINE" != "mysql" && "$DB_ENGINE" != "mariadb" && "$DB_ENGINE" != "auto" ]]; then + echo "--db must be 'mysql', 'mariadb', or 'auto'" >&2 + exit 1 + fi + + if [[ "$WEB_SERVER" != "apache" && "$WEB_SERVER" != "nginx" && "$WEB_SERVER" != "auto" ]]; then + echo "--web must be 'apache', 'nginx', or 'auto'" >&2 + exit 1 + fi +} + +set_db_engine() { + local engine="$1" + DB_ENGINE_RESOLVED="$engine" + if [[ "$engine" == "mariadb" ]]; then + DB_SERVICE="mariadb" + DB_CLI="mariadb" + DB_SERVER_PACKAGE="mariadb-server" + DB_CLIENT_PACKAGE="mariadb-client" + DB_PINGER="mysqladmin" + else + DB_SERVICE="mysql" + DB_CLI="mysql" + DB_SERVER_PACKAGE="mysql-server" + DB_CLIENT_PACKAGE="mysql-client" + DB_PINGER="mysqladmin" + fi +} + +detect_db_engine() { + if [[ "$DB_ENGINE" != "auto" ]]; then + set_db_engine "$DB_ENGINE" + return + fi + + if systemctl list-units --type=service --all | grep -q "mariadb.service"; then + set_db_engine "mariadb"; return + fi + if systemctl list-units --type=service --all | grep -q "mysql.service"; then + set_db_engine "mysql"; return + fi + if command -v mariadb >/dev/null 2>&1; then + set_db_engine "mariadb"; return + fi + if command -v mysql >/dev/null 2>&1; then + if mysql --version 2>/dev/null | grep -qi mariadb; then + set_db_engine "mariadb" + else + set_db_engine "mysql" + fi + return + fi + + set_db_engine "mysql" +} + +detect_web_server() { + if [[ "$WEB_SERVER" == "apache" || "$WEB_SERVER" == "nginx" ]]; then + WEB_SERVER_RESOLVED="$WEB_SERVER" + return + fi + + if systemctl list-units --type=service --all | grep -q "apache2.service"; then + WEB_SERVER_RESOLVED="apache"; return + fi + if systemctl list-units --type=service --all | grep -q "nginx.service"; then + WEB_SERVER_RESOLVED="nginx"; return + fi + + if command -v apache2 >/dev/null 2>&1; then + WEB_SERVER_RESOLVED="apache"; return + fi + if command -v nginx >/dev/null 2>&1; then + WEB_SERVER_RESOLVED="nginx"; return + fi + + WEB_SERVER_RESOLVED="apache" +} + +install_packages() { + echo "Updating apt package lists..." + apt-get update -y + + echo "Installing PHP + extensions..." + apt-get install -y php php-cli php-fpm php-mysql php-xml php-mbstring php-curl unzip curl git + + echo "Ensuring database server packages are present (${DB_ENGINE_RESOLVED})..." + apt-get install -y "$DB_SERVER_PACKAGE" "$DB_CLIENT_PACKAGE" + + echo "Ensuring web server packages are present (${WEB_SERVER_RESOLVED})..." + if [[ "$WEB_SERVER_RESOLVED" == "apache" ]]; then + apt-get install -y apache2 libapache2-mod-php + else + apt-get install -y nginx + fi +} + +start_services() { + echo "Starting database service (${DB_SERVICE})..." + systemctl enable --now "$DB_SERVICE" + + if [[ "$WEB_SERVER_RESOLVED" == "apache" ]]; then + echo "Starting Apache..." + systemctl enable --now apache2 + else + echo "Starting PHP-FPM and Nginx..." + systemctl enable --now "php$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')-fpm" + systemctl enable --now nginx + fi +} + +wait_for_database() { + echo "Waiting for ${DB_ENGINE_RESOLVED} to become ready..." + local retries=30 + until $DB_PINGER ping --silent >/dev/null 2>&1; do + retries=$((retries - 1)) + if [[ $retries -le 0 ]]; then + echo "Database service did not become ready in time." >&2 + exit 1 + fi + sleep 1 + done +} + +provision_database() { + echo "Creating database '$DB_NAME' and user '$DB_USER'..." + $DB_CLI -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" + $DB_CLI -e "CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS';" + $DB_CLI -e "GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';" + $DB_CLI -e "FLUSH PRIVILEGES;" + + if [[ $SKIP_SEED -eq 0 ]]; then + local seed_path="${APP_ROOT}/seed.sql" + if [[ -f "$seed_path" ]]; then + echo "Importing seed.sql into '$DB_NAME'..." + $DB_CLI "$DB_NAME" < "$seed_path" + else + echo "seed.sql not found at ${seed_path}; skipping import." >&2 + fi + else + echo "Skipping seed import as requested." + fi +} + +configure_apache() { + local app_root="$1" + cat >/etc/apache2/sites-available/opensafetymap.conf < + ServerName localhost + DocumentRoot ${app_root} + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog \${APACHE_LOG_DIR}/opensafetymap-error.log + CustomLog \${APACHE_LOG_DIR}/opensafetymap-access.log combined + +EOF + + a2dissite 000-default.conf >/dev/null 2>&1 || true + a2ensite opensafetymap.conf + a2enmod rewrite + systemctl reload apache2 +} + +configure_nginx() { + local app_root="$1" + local php_sock="/run/php/php$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')-fpm.sock" + cat >/etc/nginx/sites-available/opensafetymap <