feat: Implement Phase 4 Web Interface with bugfixes (v0.5.2)

## Phase 4: Web Interface Implementation

Implemented complete web interface with public and admin routes,
templates, CSS, and development authentication.

### Core Features

**Public Routes**:
- Homepage with recent published notes
- Note permalinks with microformats2
- Server-side rendering (Jinja2)

**Admin Routes**:
- Login via IndieLogin
- Dashboard with note management
- Create, edit, delete notes
- Protected with @require_auth decorator

**Development Authentication**:
- Dev login bypass for local testing (DEV_MODE only)
- Security safeguards per ADR-011
- Returns 404 when disabled

**Templates & Frontend**:
- Base layouts (public + admin)
- 8 HTML templates with microformats2
- Custom responsive CSS (114 lines)
- Error pages (404, 500)

### Bugfixes (v0.5.1 → v0.5.2)

1. **Cookie collision fix (v0.5.1)**:
   - Renamed auth cookie from "session" to "starpunk_session"
   - Fixed redirect loop between dev login and admin dashboard
   - Flask's session cookie no longer conflicts with auth

2. **HTTP 404 error handling (v0.5.1)**:
   - Update route now returns 404 for nonexistent notes
   - Delete route now returns 404 for nonexistent notes
   - Follows ADR-012 HTTP Error Handling Policy
   - Pattern consistency across all admin routes

3. **Note model enhancement (v0.5.2)**:
   - Exposed deleted_at field from database schema
   - Enables soft deletion verification in tests
   - Follows ADR-013 transparency principle

### Architecture

**New ADRs**:
- ADR-011: Development Authentication Mechanism
- ADR-012: HTTP Error Handling Policy
- ADR-013: Expose deleted_at Field in Note Model

**Standards Compliance**:
- Uses uv for Python environment
- Black formatted, Flake8 clean
- Follows git branching strategy
- Version incremented per versioning strategy

### Test Results

- 405/406 tests passing (99.75%)
- 87% code coverage
- All security tests passing
- Manual testing confirmed working

### Documentation

- Complete implementation reports in docs/reports/
- Architecture reviews in docs/reviews/
- Design documents in docs/design/
- CHANGELOG updated for v0.5.2

### Files Changed

**New Modules**:
- starpunk/dev_auth.py
- starpunk/routes/ (public, admin, auth, dev_auth)

**Templates**: 10 files (base, pages, admin, errors)
**Static**: CSS and optional JavaScript
**Tests**: 4 test files for routes and templates
**Docs**: 20+ architectural and implementation documents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 23:01:53 -07:00
parent 575a02186b
commit 0cca8169ce
56 changed files with 13151 additions and 304 deletions

View File

@@ -20,54 +20,104 @@ def load_config(app, config_override=None):
load_dotenv()
# Site configuration
app.config['SITE_URL'] = os.getenv('SITE_URL', 'http://localhost:5000')
app.config['SITE_NAME'] = os.getenv('SITE_NAME', 'StarPunk')
app.config['SITE_AUTHOR'] = os.getenv('SITE_AUTHOR', 'Unknown')
app.config['SITE_DESCRIPTION'] = os.getenv(
'SITE_DESCRIPTION',
'A minimal IndieWeb CMS'
app.config["SITE_URL"] = os.getenv("SITE_URL", "http://localhost:5000")
app.config["SITE_NAME"] = os.getenv("SITE_NAME", "StarPunk")
app.config["SITE_AUTHOR"] = os.getenv("SITE_AUTHOR", "Unknown")
app.config["SITE_DESCRIPTION"] = os.getenv(
"SITE_DESCRIPTION", "A minimal IndieWeb CMS"
)
# Authentication
app.config['ADMIN_ME'] = os.getenv('ADMIN_ME')
app.config['SESSION_SECRET'] = os.getenv('SESSION_SECRET')
app.config['SESSION_LIFETIME'] = int(os.getenv('SESSION_LIFETIME', '30'))
app.config['INDIELOGIN_URL'] = os.getenv(
'INDIELOGIN_URL',
'https://indielogin.com'
)
app.config["ADMIN_ME"] = os.getenv("ADMIN_ME")
app.config["SESSION_SECRET"] = os.getenv("SESSION_SECRET")
app.config["SESSION_LIFETIME"] = int(os.getenv("SESSION_LIFETIME", "30"))
app.config["INDIELOGIN_URL"] = os.getenv("INDIELOGIN_URL", "https://indielogin.com")
# Validate required configuration
if not app.config['SESSION_SECRET']:
if not app.config["SESSION_SECRET"]:
raise ValueError(
"SESSION_SECRET must be set in .env file. "
"Generate with: python3 -c \"import secrets; print(secrets.token_hex(32))\""
'Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"'
)
# Flask secret key (uses SESSION_SECRET by default)
app.config['SECRET_KEY'] = os.getenv(
'FLASK_SECRET_KEY',
app.config['SESSION_SECRET']
app.config["SECRET_KEY"] = os.getenv(
"FLASK_SECRET_KEY", app.config["SESSION_SECRET"]
)
# Data paths
app.config['DATA_PATH'] = Path(os.getenv('DATA_PATH', './data'))
app.config['NOTES_PATH'] = Path(os.getenv('NOTES_PATH', './data/notes'))
app.config['DATABASE_PATH'] = Path(
os.getenv('DATABASE_PATH', './data/starpunk.db')
)
app.config["DATA_PATH"] = Path(os.getenv("DATA_PATH", "./data"))
app.config["NOTES_PATH"] = Path(os.getenv("NOTES_PATH", "./data/notes"))
app.config["DATABASE_PATH"] = Path(os.getenv("DATABASE_PATH", "./data/starpunk.db"))
# Flask environment
app.config['ENV'] = os.getenv('FLASK_ENV', 'development')
app.config['DEBUG'] = os.getenv('FLASK_DEBUG', '1') == '1'
app.config["ENV"] = os.getenv("FLASK_ENV", "development")
app.config["DEBUG"] = os.getenv("FLASK_DEBUG", "1") == "1"
# Logging
app.config['LOG_LEVEL'] = os.getenv('LOG_LEVEL', 'INFO')
app.config["LOG_LEVEL"] = os.getenv("LOG_LEVEL", "INFO")
# Development mode configuration
app.config["DEV_MODE"] = os.getenv("DEV_MODE", "false").lower() == "true"
app.config["DEV_ADMIN_ME"] = os.getenv("DEV_ADMIN_ME", "")
# Application version
app.config["VERSION"] = os.getenv("VERSION", "0.5.0")
# Apply overrides if provided
if config_override:
app.config.update(config_override)
# Convert path strings to Path objects (in case overrides provided strings)
if isinstance(app.config["DATA_PATH"], str):
app.config["DATA_PATH"] = Path(app.config["DATA_PATH"])
if isinstance(app.config["NOTES_PATH"], str):
app.config["NOTES_PATH"] = Path(app.config["NOTES_PATH"])
if isinstance(app.config["DATABASE_PATH"], str):
app.config["DATABASE_PATH"] = Path(app.config["DATABASE_PATH"])
# Validate configuration
validate_config(app)
# Ensure data directories exist
app.config['DATA_PATH'].mkdir(parents=True, exist_ok=True)
app.config['NOTES_PATH'].mkdir(parents=True, exist_ok=True)
app.config["DATA_PATH"].mkdir(parents=True, exist_ok=True)
app.config["NOTES_PATH"].mkdir(parents=True, exist_ok=True)
def validate_config(app):
"""
Validate application configuration on startup
Ensures required configuration is present based on mode (dev/production)
and warns prominently if development mode is enabled.
Args:
app: Flask application instance
Raises:
ValueError: If required configuration is missing
"""
dev_mode = app.config.get("DEV_MODE", False)
if dev_mode:
# Prominently warn about development mode
app.logger.warning(
"=" * 60 + "\n"
"WARNING: Development authentication enabled!\n"
"This should NEVER be used in production.\n"
"Set DEV_MODE=false for production deployments.\n" + "=" * 60
)
# Require DEV_ADMIN_ME in dev mode
if not app.config.get("DEV_ADMIN_ME"):
raise ValueError(
"DEV_MODE=true requires DEV_ADMIN_ME to be set. "
"Set DEV_ADMIN_ME=https://your-dev-identity.example.com in .env"
)
else:
# Production mode: ADMIN_ME is required
if not app.config.get("ADMIN_ME"):
raise ValueError(
"Production mode requires ADMIN_ME to be set. "
"Set ADMIN_ME=https://your-site.com in .env"
)