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 hasid,slug,title,excerpt,content(HTML),date,lang,tags, and ametablock (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 topublic/. Also writespublic/api/posts.jsonas a machine-readable JSON feed.src/input.cssβ Tailwind source. Custom design tokens (surface,ink,accent) defined intailwind.config.js.tailwind.config.jsβ dark-mode viaclass,@tailwindcss/typographyfor 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 callsAIBridgefor 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_ATTEMPTSfailures withinBLOCK_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_PEPPERconstant is the single highest-impact secret β never commit a real value.