5 Commits
Author SHA1 Message Date
andrew 7a68c933ce Merge pull request 'fix: handle host-prefixed root path in PublicPathMatcher' (#8) from fix/public-path-host-root into main
Push Develop / docker (push) Successful in 4m44s
Sync GitHub / sync (push) Successful in 7s
Tests / test (push) Successful in 45s
Push Docker / docker (push) Successful in 4m43s
Reviewed-on: #8
Reviewed-by: Andrew <andrew@digitaladapt.com>
2026-08-13 16:28:05 -04:00
andrew 55f8e9e84c Merge branch 'main' into fix/public-path-host-root
Sync GitHub / sync (push) Successful in 9s
Tests / test (pull_request) Successful in 1m16s
2026-08-13 15:43:54 -04:00
lyra e3cd8c6739 fix: handle host-prefixed root path in PublicPathMatcher
Sync GitHub / sync (push) Successful in 6s
Tests / test (pull_request) Successful in 1m3s
The host-prefix regex required at least one character after the slash
(/\+.+/), so a pattern like 'code.example.com/' was silently dropped
instead of matching the root path '/'. Changed \+.+ to \+.* so the
trailing slash alone is accepted as the path '/'.

Added tests covering the exact bug scenario from PUBLIC_PATHS
config: 'code.digitaladapt.com/,code.digitaladapt.com/public/**'
2026-08-13 14:30:52 -04:00
lyra 235a7866b3 fix: remove incorrect CSS escaping on color values
Tests / test (pull_request) Successful in 1m5s
Push Develop / docker (push) Successful in 6m43s
Sync GitHub / sync (push) Successful in 8s
Tests / test (push) Successful in 54s
The Twig |e('css') filter was escaping '#' (0x23) to '\23 ' in hex
color values (bg_color, fg_color, error_color), causing browsers to
not recognize them as valid CSS colors. These are admin-configured
environment variables, not user input, so CSS escaping is unnecessary.
2026-08-13 12:07:20 -04:00
lyra c7585e720a feat: add self-bootstrapping dev server script (bin/dev.sh)
Tests / test (pull_request) Successful in 50s
Push Develop / docker (push) Successful in 8m39s
Sync GitHub / sync (push) Successful in 7s
Tests / test (push) Successful in 57s
Manages a local PHP dev server for end-to-end development and testing.
Binds to 0.0.0.0:8773, accessible via Caddy at
https://preauth.lyra-dev.devgnome.com.

Features:
- Self-bootstrapping: installs PHP 8.4 + extensions (including APCu,
  which is critical for nonce cache, rate limiter, and session storage),
  Composer, and project dependencies if missing. Survives terminal
  resets/reboots.
- Enables apc.enable_cli=1 for console commands (matches Dockerfile)
- Subcommands: start, stop, status, restart
- Sets APP_SHARE_DIR to var/share for filesystem session persistence
- Clears dev cache on start

No database needed — preauth uses APCu + filesystem cache exclusively.

Port assignment: P-R-E = 7-7-3 → 8773
2026-08-13 11:56:55 -04:00
4 changed files with 287 additions and 3 deletions
Executable
+265
View File
@@ -0,0 +1,265 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# PreAuth Dev Server Script
#
# Manages a local PHP dev server for end-to-end development and testing.
# Binds to 0.0.0.0 so the app is accessible via a reverse proxy (Caddy) for
# browser-based visual verification.
#
# PreAuth is a TOTP-based authentication gateway. It uses APCu for nonce/cache
# and filesystem for session persistence — no database needed. The dev server
# runs with APP_ENV=dev and APP_DEBUG=1 for live troubleshooting.
#
# Self-bootstrapping: the `start` command checks for required system packages
# (PHP, extensions, tools), Composer, and project dependencies — installing
# them automatically if missing. This means the script works even after a
# terminal reset/reboot, embracing the self-cleaning container design.
#
# Usage:
# bin/dev.sh start Start the dev server (auto-installs deps if needed)
# bin/dev.sh stop Stop the dev server
# bin/dev.sh status Check if the dev server is running
# bin/dev.sh restart Stop and start the dev server
#
# Port assignment (P-R-E = 7-7-3):
# 8773 → https://preauth.lyra-dev.devgnome.com
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Configuration ───────────────────────────────────────────────────────────
PORT=8773
HOST="0.0.0.0"
ENV="dev"
DEV_SECRET="dev_secret_not_for_production_use_only"
PID_FILE="var/.dev-server.pid"
LOG_FILE="var/log/dev-server.log"
# Required PHP extensions (checked via php -m)
REQUIRED_PHP_EXTS=(
ctype
iconv
mbstring
apcu
dom
SimpleXML
xml
)
# Apt packages for PHP + extensions
# Note: preauth uses Symfony 7.4 which requires PHP >=8.1.
# We install PHP 8.4 (available in Debian 13/Trixie) for consistency.
PHP_APT_PACKAGES=(
php8.4-cli
php8.4-common # ctype, iconv
php8.4-mbstring
php8.4-xml # dom, SimpleXML, xml
php8.4-opcache
php8.4-readline
php8.4-apcu # APCu — critical for nonce cache, rate limiter, sessions
)
# System tools needed
SYSTEM_TOOLS=(
git
unzip
curl
)
# Resolve project root (script lives in bin/)
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"
# Ensure var directory structure exists
mkdir -p var/log var/share
# ── Helpers ─────────────────────────────────────────────────────────────────
is_running() {
if [[ ! -f "$PID_FILE" ]]; then
return 1
fi
local pid
pid="$(cat "$PID_FILE")"
if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
return 1
fi
return 0
}
print_status() {
if is_running; then
local pid
pid="$(cat "$PID_FILE")"
echo "✅ PreAuth dev server is RUNNING"
echo " PID: $pid"
echo " URL: http://localhost:${PORT}"
echo " Exposed: http://${HOST}:${PORT}"
echo " Dev URL: https://preauth.lyra-dev.devgnome.com"
echo " Logs: ${LOG_FILE}"
else
echo "⛔ PreAuth dev server is STOPPED"
fi
}
# ── Bootstrap ───────────────────────────────────────────────────────────────
# Ensures all system packages, Composer, and project dependencies are present.
# Idempotent — if everything is already installed, checks are fast no-ops.
# This is what makes the script survive terminal resets/reboots.
bootstrap() {
local needed_packages=()
# ── Check system tools ──
for tool in "${SYSTEM_TOOLS[@]}"; do
if ! command -v "$tool" &>/dev/null; then
needed_packages+=("$tool")
fi
done
# ── Check PHP and required extensions ──
local php_needs_install=false
if ! command -v php &>/dev/null; then
php_needs_install=true
else
for ext in "${REQUIRED_PHP_EXTS[@]}"; do
if ! php -m 2>/dev/null | grep -iq "^${ext}$"; then
php_needs_install=true
break
fi
done
fi
if [[ "$php_needs_install" == "true" ]]; then
needed_packages+=("${PHP_APT_PACKAGES[@]}")
fi
# ── Install missing packages ──
if [[ ${#needed_packages[@]} -gt 0 ]]; then
echo "→ Installing missing system packages: ${needed_packages[*]}"
sudo apt-get update -qq
sudo apt-get install -y -qq "${needed_packages[@]}"
fi
# ── Ensure APCu is enabled for CLI ──
# PreAuth's console commands need APCu; the Dockerfile sets apc.enable_cli=1
local apcu_ini="/etc/php/8.4/mods-available/apcu.ini"
if [[ -f "$apcu_ini" ]] && ! grep -q 'apc.enable_cli' "$apcu_ini" 2>/dev/null; then
echo "→ Enabling APCu CLI support…"
echo 'apc.enable_cli=1' | sudo tee -a "$apcu_ini" >/dev/null
fi
# ── Ensure Composer is available ──
if ! command -v composer &>/dev/null; then
echo "→ Installing Composer…"
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
sudo chmod +x /usr/local/bin/composer
fi
# ── Ensure project dependencies are installed ──
if [[ ! -d "vendor/" ]]; then
echo "→ Installing Composer dependencies…"
APP_ENV=dev composer install --no-interaction
fi
}
# ── Commands ────────────────────────────────────────────────────────────────
start() {
if is_running; then
echo "⚠️ Dev server is already running (PID $(cat "$PID_FILE"))"
print_status
exit 0
fi
echo "→ Starting PreAuth dev server on ${HOST}:${PORT}"
# Self-bootstrap: ensure all dependencies are present
bootstrap
echo "→ Clearing dev cache…"
APP_ENV="$ENV" \
APP_DEBUG=1 \
APP_SECRET="$DEV_SECRET" \
php bin/console cache:clear 2>&1 | tail -3
echo "→ Starting PHP dev server…"
APP_ENV="$ENV" \
APP_DEBUG=1 \
APP_SECRET="$DEV_SECRET" \
APP_SHARE_DIR="${PROJECT_ROOT}/var/share" \
nohup php -S "${HOST}:${PORT}" -t public/ > "$LOG_FILE" 2>&1 &
local pid=$!
echo "$pid" > "$PID_FILE"
# Give it a moment to boot
sleep 2
if is_running; then
echo ""
print_status
else
echo "❌ Failed to start dev server. Check logs:"
echo " ${LOG_FILE}"
tail -20 "$LOG_FILE" 2>/dev/null || true
rm -f "$PID_FILE"
exit 1
fi
}
stop() {
if ! is_running; then
echo "⚠️ Dev server is not running."
rm -f "$PID_FILE"
exit 0
fi
local pid
pid="$(cat "$PID_FILE")"
echo "→ Stopping dev server (PID ${pid})…"
kill "$pid" 2>/dev/null || true
# Wait for graceful shutdown
local count=0
while kill -0 "$pid" 2>/dev/null && [[ $count -lt 10 ]]; do
sleep 0.5
count=$((count + 1))
done
# Force kill if still alive
if kill -0 "$pid" 2>/dev/null; then
echo "→ Process didn't exit gracefully, sending SIGKILL…"
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$PID_FILE"
echo "✅ Dev server stopped."
}
restart() {
stop
sleep 1
start
}
# ── Main ────────────────────────────────────────────────────────────────────
usage() {
echo "Usage: bin/dev.sh {start|stop|status|restart}"
echo ""
echo "Commands:"
echo " start Start the dev server (auto-installs deps if needed)"
echo " stop Stop the dev server"
echo " status Check if the dev server is running"
echo " restart Restart the dev server"
exit 1
}
case "${1:-}" in
start) start ;;
stop) stop ;;
status) print_status ;;
restart) restart ;;
*) usage ;;
esac
+1 -1
View File
@@ -79,7 +79,7 @@ final readonly class PublicPathMatcher implements PublicPathMatcherInterface
$host = null;
$path = $entry;
if (preg_match('/^([a-z0-9.-]+)(\/.+)$/i', $entry, $m)) {
if (preg_match('/^([a-z0-9.-]+)(\/.*)$/i', $entry, $m)) {
$host = strtolower($m[1]);
$path = $m[2];
}
+2 -2
View File
@@ -1,10 +1,10 @@
<style id="preauth-style">
* { margin: 0; padding: 0.25em; }
html { background-color: {{ env.bg_color|e('css') }}; color: {{ env.fg_color|e('css') }}; display: table;
html { background-color: {{ env.bg_color }}; color: {{ env.fg_color }}; display: table;
font-family: sans-serif; font-size: 1.5em; height: 100%; padding: 0; width: 100%; }
body { display: table-cell; vertical-align: middle; }
h1 { font-size: 2.5em; font-weight: normal; text-align: center; }
p { color: {{ env.error_color|e('css') }}; text-align: center; }
p { color: {{ env.error_color }}; text-align: center; }
form { align-items: baseline; display: flex; flex-wrap: wrap; justify-content: center; }
form div { width: 45%; min-width: 300px; }
div.right { text-align: right; margin-top: 1em; padding-bottom: 0 }
@@ -176,6 +176,25 @@ final class PublicPathMatcherTest extends TestCase
self::assertFalse($matcher->matches('other.host', '/public/repo'));
}
public function testDomainPrefixedRootPathMatchesRoot(): void
{
// host/ — the trailing slash is the entire path, nothing after it
$matcher = new PublicPathMatcher('code.example.com/');
self::assertTrue($matcher->matches('code.example.com', '/'));
self::assertFalse($matcher->matches('code.example.com', '/public'));
self::assertFalse($matcher->matches('other.example.com', '/'));
}
public function testDomainPrefixedRootWithOtherPatterns(): void
{
// The exact scenario from the bug report
$matcher = new PublicPathMatcher('code.example.com/,code.example.com/public/**');
self::assertTrue($matcher->matches('code.example.com', '/'));
self::assertTrue($matcher->matches('code.example.com', '/public/repo'));
self::assertFalse($matcher->matches('code.example.com', '/private'));
self::assertFalse($matcher->matches('other.example.com', '/'));
}
public function testDomainPrefixIsCaseInsensitive(): void
{
$matcher = new PublicPathMatcher('Code.Example.COM/public/**');