CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What This Is

Retro PWD Reset (v2.0-HYBRID) β€” a no-framework PHP 8.3+ authentication and password-reset application. It supports MySQL (legacy), PostgreSQL (primary), and SQLite (tests only) through an adapter pattern, plus an optional Ollama-backed AI bridge and a WordPress content importer.

Commands

Frontend (Node / Tailwind)

npm run build        # compile Tailwind CSS + generate all HTML from content/*.json
npm run build:css    # CSS only (minified)
npm run build:html   # HTML only (node build.js)
npm run dev          # watch mode β€” recompiles CSS on src/ changes

Serve the result with any static server, e.g.:

npx serve public/

PHP Backend

Run all unit tests (SQLite in-memory, no live DB needed):

php phpunit.phar --configuration phpunit.xml

Run a single test file:

php phpunit.phar tests/AuthTest.php

Run integration tests (requires a live DB matching config.php):

php phpunit.phar --configuration phpunit_integration.xml

Deploy / generate config.php interactively:

bash deploy_retro.sh

Seed the admin user (PostgreSQL):

php seed_pg.php

Apply schema:

# PostgreSQL
sudo -u postgres psql -d retro_pwd -f schema_pg.sql
# MySQL
mysql -u root -p retro_pwd < schema.sql

Configuration

config.php is generated from config.sample.php by deploy_retro.sh. Key constants:

Constant Values Notes
DB_TYPE 'mysql' / 'pgsql' / 'sqlite' Drives adapter selection in Database singleton
DB_PORT 3306 / 5432
PASSWORD_PEPPER string Must match the value used when seeding β€” changing it invalidates all passwords
MAIL_TYPE 'file' / 'mail' / 'smtp' 'file' logs to email_log.txt
$ENABLED_MODULES array of bools Gates user_crud, blog, importer modules

Architecture

Static frontend pipeline

WP MySQL β†’ NullfeldImporter β†’ local DB β†’ export.php β†’ content/posts.json
                                                              ↓
                                                          build.js (Node)
                                                              ↓
                                              public/index.html + public/posts/*.html
                                              public/api/posts.json  (JSON feed)
  • content/posts.json β€” source of truth for the frontend. Each post has id, slug, title, excerpt, content (HTML), date, lang, tags, and a meta block (description, keywords, json_ld).
  • content/site.json β€” global site title, nav links, author.
  • build.js β€” reads both JSON files, renders HTML via JS template literals (no extra deps), writes to public/. Also writes public/api/posts.json as a machine-readable JSON feed.
  • src/input.css β€” Tailwind source. Custom design tokens (surface, ink, accent) defined in tailwind.config.js.
  • tailwind.config.js β€” dark-mode via class, @tailwindcss/typography for prose content, custom color palette.

The generated public/ is fully static β€” no PHP at serve time. AI enrichment (meta.keywords, meta.json_ld) is added at export time, not build time.

PHP backend request flow

Entry points (index.php, forgot_password.php, reset_password.php, dashboard.php) bootstrap session_start(), instantiate Auth / Language / View, handle POST, then call View::render($template, $data).

View::render loads templates/layout.php which embeds the per-page template (templates/login.php, templates/forgot.php, etc.).

Database layer (classes/)

Database is a singleton that selects and wraps one of three PDO adapters at construction time:
- MySQLAdapter β†’ PDO MySQL
- PostgresAdapter β†’ PDO PgSQL
- SQLiteAdapter β†’ PDO SQLite (used only in unit tests via phpunit.xml)

All adapters implement DatabaseAdapterInterface (connect(), query(), lastInsertId(), getConnection()). Callers always go through Database::getInstance()->query($sql, $params).

In tests, Database::setInstance($mockDb) replaces the singleton with MockDatabase (an in-memory SQLite instance pre-seeded with the app schema).

Auth (classes/Auth.php)

Handles login (with IP-based rate limiting via Logger), password reset token lifecycle, and session writes. Passwords are stored as bcrypt(plaintext + PASSWORD_PEPPER).

Modules (optional, feature-flagged in $ENABLED_MODULES)

  • modules/blog/ β€” minimalist CMS (CRUD for posts, categories, tags).
  • modules/user_crud/ β€” admin user management.
  • modules/importer/ β€” NullfeldImporter: migrates WordPress MySQL posts into the local DB, downloads remote images, and optionally calls AIBridge for keyword/summary extraction.

AI Bridge (classes/AIBridge.php)

Calls Ollama's /api/chat endpoint (default: http://localhost:11434/api/chat, model llama3). Used by NullfeldImporter during import to enrich post content. Requires a running Ollama instance β€” gracefully returns an error string if unavailable.

Internationalisation (classes/Language.php, lang/)

Language loads lang/en.php or lang/de.php (key→string maps). Templates call $lang->get('key').

Security model (see docs/SECURITY_CONCEPT.md)

  • Brute-force: IP blocked after MAX_LOGIN_ATTEMPTS failures within BLOCK_DURATION_MINUTES. Block clears on successful password reset.
  • Reset tokens: 64-char hex from openssl_random_pseudo_bytes(32), stored plain (short TTL accepted trade-off), deleted on use.
  • The PASSWORD_PEPPER constant is the single highest-impact secret β€” never commit a real value.