## 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>
1315 lines
34 KiB
Markdown
1315 lines
34 KiB
Markdown
# Phase 4: Web Interface - Implementation Design
|
|
|
|
**Date**: 2025-11-18
|
|
**Phase**: 4
|
|
**Status**: Design Complete, Ready for Implementation
|
|
**Dependencies**: Phase 3 (Authentication) complete
|
|
**Version**: 0.5.0 target
|
|
|
|
## Executive Summary
|
|
|
|
Phase 4 implements the complete web interface for StarPunk, including public routes for viewing notes and admin routes for managing content. This phase brings together all previous work (core utilities, data models, notes management, authentication) into a cohesive user-facing application.
|
|
|
|
**Key Deliverables**:
|
|
- Public interface (homepage, note permalinks)
|
|
- Admin interface (dashboard, note editor, login/logout)
|
|
- Development authentication mechanism
|
|
- Template system with microformats
|
|
- Basic CSS styling
|
|
- Optional JavaScript enhancements
|
|
|
|
## Scope
|
|
|
|
### In Scope for Phase 4
|
|
|
|
**Public Routes**:
|
|
- Homepage with recent notes
|
|
- Individual note permalinks
|
|
- Feed discovery (RSS link)
|
|
|
|
**Admin Routes**:
|
|
- Login page
|
|
- Admin dashboard (note list)
|
|
- Create note form
|
|
- Edit note form
|
|
- Delete note confirmation
|
|
- Logout
|
|
|
|
**Authentication Integration**:
|
|
- Production auth (IndieLogin)
|
|
- Development auth mechanism (new)
|
|
- Session management
|
|
- Protected route enforcement
|
|
|
|
**Frontend**:
|
|
- Jinja2 templates
|
|
- Custom CSS (no framework)
|
|
- Optional JavaScript (markdown preview)
|
|
- Microformats2 markup
|
|
|
|
**Development Tools**:
|
|
- Dev auth for local testing
|
|
- Configuration validation
|
|
- Development mode indicators
|
|
|
|
### Out of Scope
|
|
|
|
**Deferred to Future Phases**:
|
|
- RSS feed generation (Phase 5)
|
|
- Micropub endpoint (Phase 6)
|
|
- API routes (Phase 7)
|
|
- Media uploads
|
|
- Advanced search
|
|
- Tags/categories
|
|
- Themes/customization
|
|
|
|
## Architecture Overview
|
|
|
|
### Component Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Web Interface Layer │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Public Routes│ │ Admin Routes │ │
|
|
│ ├──────────────┤ ├──────────────┤ │
|
|
│ │ / │ │ /admin/login │ │
|
|
│ │ /note/<slug> │ │ /admin │ │
|
|
│ │ │ │ /admin/new │ │
|
|
│ │ │ │ /admin/edit │ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
│ │ │ │
|
|
│ │ │ │
|
|
│ ├──────────────────────────────┤ │
|
|
│ │ Template Rendering │ │
|
|
│ │ (Jinja2) │ │
|
|
│ └──────────────────────────────┘ │
|
|
│ │ │
|
|
└────────────────────────┼─────────────────────────────────┘
|
|
│
|
|
↓
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Business Logic Layer │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐│
|
|
│ │ Notes Module │ │ Auth Module │ │ Dev Auth ││
|
|
│ │ (existing) │ │ (existing) │ │ (new) ││
|
|
│ └──────────────┘ └──────────────┘ └──────────────┘│
|
|
└─────────────────────────────────────────────────────────┘
|
|
│
|
|
↓
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Data Layer │
|
|
├─────────────────────────────────────────────────────────┤
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Markdown │ │ SQLite DB │ │
|
|
│ │ Files │ │ (metadata) │ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Request Flow
|
|
|
|
**Public Note View**:
|
|
```
|
|
User → GET /note/my-first-note
|
|
↓
|
|
Route Handler → get_note_by_slug('my-first-note')
|
|
↓
|
|
Notes Module → Query database for metadata
|
|
↓
|
|
Notes Module → Read markdown file
|
|
↓
|
|
Notes Module → Render markdown to HTML
|
|
↓
|
|
Template Render → note.html with microformats
|
|
↓
|
|
User ← HTML Response
|
|
```
|
|
|
|
**Admin Note Creation**:
|
|
```
|
|
User → GET /admin/new
|
|
↓
|
|
Auth Check → require_auth decorator
|
|
↓
|
|
Session Valid? → No → Redirect to /admin/login
|
|
→ Yes ↓
|
|
Route Handler → Render new.html form
|
|
↓
|
|
User ← HTML Form
|
|
|
|
User → POST /admin/new (markdown content)
|
|
↓
|
|
Auth Check → require_auth decorator
|
|
↓
|
|
Route Handler → create_note(content, published=True)
|
|
↓
|
|
Notes Module → Generate slug, write file, insert DB
|
|
↓
|
|
Route Handler → Redirect to /admin (dashboard)
|
|
↓
|
|
User ← Redirect with flash message
|
|
```
|
|
|
|
## Routes Specification
|
|
|
|
### Public Routes
|
|
|
|
#### `GET /`
|
|
**Purpose**: Homepage showing recent published notes
|
|
|
|
**Handler**: `public.index()`
|
|
|
|
**Logic**:
|
|
```python
|
|
def index():
|
|
notes = get_all_notes(
|
|
published=True,
|
|
limit=20,
|
|
order_by='created_at DESC'
|
|
)
|
|
return render_template('index.html', notes=notes)
|
|
```
|
|
|
|
**Template**: `templates/index.html`
|
|
- List of notes (h-feed)
|
|
- Each note: title/content preview (h-entry)
|
|
- Pagination (if >20 notes)
|
|
- Link to RSS feed
|
|
|
|
**Response**:
|
|
- 200 OK with HTML
|
|
- Content-Type: text/html
|
|
- Microformats: h-feed with h-entry children
|
|
|
|
---
|
|
|
|
#### `GET /note/<slug>`
|
|
**Purpose**: Individual note permalink
|
|
|
|
**Handler**: `public.note(slug)`
|
|
|
|
**Logic**:
|
|
```python
|
|
def note(slug):
|
|
note = get_note_by_slug(slug)
|
|
if not note or not note.published:
|
|
abort(404)
|
|
return render_template('note.html', note=note)
|
|
```
|
|
|
|
**Template**: `templates/note.html`
|
|
- Full note content (h-entry)
|
|
- Publication date
|
|
- Permalink URL
|
|
- Microformats markup
|
|
|
|
**Response**:
|
|
- 200 OK with HTML (note found)
|
|
- 404 Not Found (unpublished or missing)
|
|
- Content-Type: text/html
|
|
|
|
**Microformats**:
|
|
```html
|
|
<article class="h-entry">
|
|
<div class="e-content">{{ note.html|safe }}</div>
|
|
<footer>
|
|
<a class="u-url" href="{{ url_for('public.note', slug=note.slug) }}">
|
|
<time class="dt-published" datetime="{{ note.created_at.isoformat() }}">
|
|
{{ note.created_at.strftime('%B %d, %Y') }}
|
|
</time>
|
|
</a>
|
|
</footer>
|
|
</article>
|
|
```
|
|
|
|
---
|
|
|
|
### Admin Routes (Protected)
|
|
|
|
All admin routes require authentication via `@require_auth` decorator.
|
|
|
|
#### `GET /admin/login`
|
|
**Purpose**: Display login form
|
|
|
|
**Handler**: `auth_routes.login_form()`
|
|
|
|
**Logic**:
|
|
```python
|
|
def login_form():
|
|
# If already logged in, redirect to dashboard
|
|
session_token = request.cookies.get('session')
|
|
if session_token and verify_session(session_token):
|
|
return redirect(url_for('admin.dashboard'))
|
|
|
|
return render_template('admin/login.html')
|
|
```
|
|
|
|
**Template**: `templates/admin/login.html`
|
|
- Form with "me" URL input
|
|
- Submit to POST /admin/login
|
|
- Link to IndieLogin documentation
|
|
- If DEV_MODE: show dev login link
|
|
|
|
**Response**: 200 OK with HTML
|
|
|
|
---
|
|
|
|
#### `POST /admin/login`
|
|
**Purpose**: Initiate IndieLogin authentication
|
|
|
|
**Handler**: `auth_routes.login_initiate()`
|
|
|
|
**Logic**:
|
|
```python
|
|
def login_initiate():
|
|
me_url = request.form.get('me')
|
|
try:
|
|
auth_url = initiate_login(me_url)
|
|
return redirect(auth_url)
|
|
except ValueError as e:
|
|
flash(str(e), 'error')
|
|
return redirect(url_for('auth_routes.login_form'))
|
|
```
|
|
|
|
**Response**:
|
|
- 302 Redirect to IndieLogin.com
|
|
- Or 302 back to login form with error
|
|
|
|
---
|
|
|
|
#### `GET /auth/callback`
|
|
**Purpose**: Handle IndieLogin callback
|
|
|
|
**Handler**: `auth_routes.callback()`
|
|
|
|
**Logic**:
|
|
```python
|
|
def callback():
|
|
code = request.args.get('code')
|
|
state = request.args.get('state')
|
|
|
|
try:
|
|
session_token = handle_callback(code, state)
|
|
response = redirect(url_for('admin.dashboard'))
|
|
response.set_cookie('session', session_token,
|
|
httponly=True, secure=True,
|
|
samesite='Lax', max_age=30*24*60*60)
|
|
flash('Login successful!', 'success')
|
|
return response
|
|
except (InvalidStateError, UnauthorizedError, IndieLoginError) as e:
|
|
flash(f'Login failed: {e}', 'error')
|
|
return redirect(url_for('auth_routes.login_form'))
|
|
```
|
|
|
|
**Response**:
|
|
- 302 Redirect to /admin with session cookie
|
|
- Or 302 to login with error
|
|
|
|
---
|
|
|
|
#### `POST /admin/logout`
|
|
**Purpose**: Destroy session and logout
|
|
|
|
**Handler**: `auth_routes.logout()`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def logout():
|
|
session_token = request.cookies.get('session')
|
|
if session_token:
|
|
destroy_session(session_token)
|
|
|
|
response = redirect(url_for('public.index'))
|
|
response.delete_cookie('session')
|
|
flash('Logged out successfully', 'success')
|
|
return response
|
|
```
|
|
|
|
**Response**: 302 Redirect to homepage
|
|
|
|
---
|
|
|
|
#### `GET /admin`
|
|
**Purpose**: Admin dashboard with note list
|
|
|
|
**Handler**: `admin.dashboard()`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def dashboard():
|
|
notes = get_all_notes(order_by='created_at DESC')
|
|
return render_template('admin/dashboard.html',
|
|
notes=notes,
|
|
user_me=g.user_me)
|
|
```
|
|
|
|
**Template**: `templates/admin/dashboard.html`
|
|
- List all notes (published and drafts)
|
|
- Each note: title, date, status, edit/delete buttons
|
|
- "New Note" button
|
|
- Logout link
|
|
- If DEV_MODE: warning banner
|
|
|
|
**Response**: 200 OK with HTML
|
|
|
|
---
|
|
|
|
#### `GET /admin/new`
|
|
**Purpose**: Create new note form
|
|
|
|
**Handler**: `admin.new_note_form()`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def new_note_form():
|
|
return render_template('admin/new.html')
|
|
```
|
|
|
|
**Template**: `templates/admin/new.html`
|
|
- Markdown textarea
|
|
- Published checkbox (default: checked)
|
|
- Submit button
|
|
- Cancel link to dashboard
|
|
- Optional: JavaScript markdown preview
|
|
|
|
**Response**: 200 OK with HTML
|
|
|
|
---
|
|
|
|
#### `POST /admin/new`
|
|
**Purpose**: Create new note
|
|
|
|
**Handler**: `admin.create_note_submit()`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def create_note_submit():
|
|
content = request.form.get('content')
|
|
published = 'published' in request.form
|
|
|
|
try:
|
|
note = create_note(content, published)
|
|
flash(f'Note created: {note.slug}', 'success')
|
|
return redirect(url_for('admin.dashboard'))
|
|
except ValueError as e:
|
|
flash(f'Error creating note: {e}', 'error')
|
|
return redirect(url_for('admin.new_note_form'))
|
|
```
|
|
|
|
**Response**:
|
|
- 302 Redirect to dashboard (success)
|
|
- 302 Redirect to form (error)
|
|
|
|
---
|
|
|
|
#### `GET /admin/edit/<int:note_id>`
|
|
**Purpose**: Edit note form
|
|
|
|
**Handler**: `admin.edit_note_form(note_id)`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def edit_note_form(note_id):
|
|
note = get_note_by_id(note_id)
|
|
if not note:
|
|
abort(404)
|
|
return render_template('admin/edit.html', note=note)
|
|
```
|
|
|
|
**Template**: `templates/admin/edit.html`
|
|
- Pre-filled markdown textarea
|
|
- Published checkbox
|
|
- Save button
|
|
- Delete button (confirmation)
|
|
- Cancel link
|
|
|
|
**Response**:
|
|
- 200 OK with HTML (note found)
|
|
- 404 Not Found (note missing)
|
|
|
|
---
|
|
|
|
#### `POST /admin/edit/<int:note_id>`
|
|
**Purpose**: Update note
|
|
|
|
**Handler**: `admin.update_note_submit(note_id)`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def update_note_submit(note_id):
|
|
content = request.form.get('content')
|
|
published = 'published' in request.form
|
|
|
|
try:
|
|
note = update_note(note_id, content, published)
|
|
flash(f'Note updated: {note.slug}', 'success')
|
|
return redirect(url_for('admin.dashboard'))
|
|
except ValueError as e:
|
|
flash(f'Error updating note: {e}', 'error')
|
|
return redirect(url_for('admin.edit_note_form', note_id=note_id))
|
|
```
|
|
|
|
**Response**:
|
|
- 302 Redirect to dashboard (success)
|
|
- 302 Redirect to form (error)
|
|
|
|
---
|
|
|
|
#### `POST /admin/delete/<int:note_id>`
|
|
**Purpose**: Delete note
|
|
|
|
**Handler**: `admin.delete_note_submit(note_id)`
|
|
**Protection**: `@require_auth`
|
|
|
|
**Logic**:
|
|
```python
|
|
@require_auth
|
|
def delete_note_submit(note_id):
|
|
if request.form.get('confirm') != 'yes':
|
|
flash('Deletion cancelled', 'info')
|
|
return redirect(url_for('admin.dashboard'))
|
|
|
|
try:
|
|
delete_note(note_id)
|
|
flash('Note deleted', 'success')
|
|
except ValueError as e:
|
|
flash(f'Error deleting note: {e}', 'error')
|
|
|
|
return redirect(url_for('admin.dashboard'))
|
|
```
|
|
|
|
**Response**: 302 Redirect to dashboard
|
|
|
|
---
|
|
|
|
### Development Routes (DEV_MODE only)
|
|
|
|
#### `GET /dev/login`
|
|
**Purpose**: Development mode instant login
|
|
|
|
**Handler**: `dev_auth.dev_login()`
|
|
**Enabled**: Only if `DEV_MODE=true`
|
|
|
|
**Logic**:
|
|
```python
|
|
def dev_login():
|
|
if not current_app.config.get('DEV_MODE'):
|
|
abort(404) # Route doesn't exist in production
|
|
|
|
me = current_app.config.get('DEV_ADMIN_ME')
|
|
session_token = create_session(me)
|
|
|
|
current_app.logger.warning(
|
|
f"DEV MODE: Created session for {me} without authentication"
|
|
)
|
|
|
|
response = redirect(url_for('admin.dashboard'))
|
|
response.set_cookie('session', session_token,
|
|
httponly=True, secure=False,
|
|
samesite='Lax', max_age=30*24*60*60)
|
|
flash('DEV MODE: Logged in without authentication', 'warning')
|
|
return response
|
|
```
|
|
|
|
**Response**: 302 Redirect to /admin with session cookie
|
|
|
|
**Security**: Returns 404 if DEV_MODE disabled
|
|
|
|
---
|
|
|
|
## Template Structure
|
|
|
|
### Base Templates
|
|
|
|
#### `templates/base.html`
|
|
**Purpose**: Base layout for public pages
|
|
|
|
**Structure**:
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}StarPunk{% endblock %}</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
<link rel="alternate" type="application/rss+xml" href="/feed.xml">
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
{% if config.DEV_MODE %}
|
|
<div class="dev-mode-warning">
|
|
⚠️ DEVELOPMENT MODE - Authentication bypassed
|
|
</div>
|
|
{% endif %}
|
|
|
|
<header>
|
|
<h1><a href="/">StarPunk</a></h1>
|
|
<nav>
|
|
<a href="/">Home</a>
|
|
<a href="/feed.xml">RSS</a>
|
|
</nav>
|
|
</header>
|
|
|
|
<main>
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="flash flash-{{ category }}">{{ message }}</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
<footer>
|
|
<p>StarPunk v{{ config.VERSION }}</p>
|
|
</footer>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
#### `templates/admin/base.html`
|
|
**Purpose**: Base layout for admin pages
|
|
|
|
**Extends**: `base.html`
|
|
|
|
**Additions**:
|
|
- Admin navigation (Dashboard, New Note, Logout)
|
|
- User identity display
|
|
- Admin-specific styling
|
|
|
|
---
|
|
|
|
### Public Templates
|
|
|
|
#### `templates/index.html`
|
|
**Purpose**: Homepage with note list
|
|
|
|
**Microformats**: h-feed containing h-entry items
|
|
|
|
**Key Elements**:
|
|
- Note list (latest 20)
|
|
- Each note: preview, date, permalink
|
|
- RSS feed link
|
|
- If empty: welcome message
|
|
|
|
---
|
|
|
|
#### `templates/note.html`
|
|
**Purpose**: Single note display
|
|
|
|
**Microformats**: h-entry
|
|
|
|
**Key Elements**:
|
|
- Full note content (rendered markdown)
|
|
- Publication date
|
|
- Permalink URL
|
|
- Back to home link
|
|
|
|
---
|
|
|
|
### Admin Templates
|
|
|
|
#### `templates/admin/login.html`
|
|
**Purpose**: Login form
|
|
|
|
**Key Elements**:
|
|
- "me" URL input field
|
|
- Submit button
|
|
- Link to IndieLogin docs
|
|
- If DEV_MODE: "Quick Dev Login" link
|
|
|
|
---
|
|
|
|
#### `templates/admin/dashboard.html`
|
|
**Purpose**: Admin note list
|
|
|
|
**Key Elements**:
|
|
- Welcome message with user identity
|
|
- "New Note" button
|
|
- Table of all notes:
|
|
- Title/content preview
|
|
- Created date
|
|
- Status (published/draft)
|
|
- Edit button
|
|
- Delete button (with confirmation)
|
|
- Logout link
|
|
|
|
---
|
|
|
|
#### `templates/admin/new.html`
|
|
**Purpose**: Create note form
|
|
|
|
**Key Elements**:
|
|
- Markdown textarea
|
|
- Published checkbox (default checked)
|
|
- Character/word count (optional JS)
|
|
- Submit button
|
|
- Cancel link
|
|
- Optional: Live markdown preview (JS)
|
|
|
|
---
|
|
|
|
#### `templates/admin/edit.html`
|
|
**Purpose**: Edit note form
|
|
|
|
**Similar to**: new.html
|
|
|
|
**Additional Elements**:
|
|
- Pre-filled content
|
|
- Note metadata display (created date, slug)
|
|
- Delete button
|
|
- Update button
|
|
|
|
---
|
|
|
|
## CSS Architecture
|
|
|
|
### File: `static/css/style.css`
|
|
|
|
**Philosophy**:
|
|
- Mobile-first responsive design
|
|
- CSS custom properties for theming
|
|
- ~300-400 lines total
|
|
- No framework dependencies
|
|
- Semantic class names
|
|
|
|
**Structure**:
|
|
```css
|
|
/* === Variables === */
|
|
:root {
|
|
/* Colors */
|
|
--color-text: #333;
|
|
--color-bg: #fff;
|
|
--color-link: #0066cc;
|
|
--color-border: #ddd;
|
|
--color-success: #28a745;
|
|
--color-error: #dc3545;
|
|
--color-warning: #ffc107;
|
|
|
|
/* Typography */
|
|
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
--font-mono: 'SF Mono', Monaco, 'Courier New', monospace;
|
|
--font-size-base: 16px;
|
|
--line-height: 1.6;
|
|
|
|
/* Spacing */
|
|
--spacing-xs: 0.25rem;
|
|
--spacing-sm: 0.5rem;
|
|
--spacing-md: 1rem;
|
|
--spacing-lg: 2rem;
|
|
--spacing-xl: 4rem;
|
|
|
|
/* Layout */
|
|
--max-width: 42rem;
|
|
--border-radius: 4px;
|
|
}
|
|
|
|
/* === Reset & Base === */
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: var(--font-body);
|
|
font-size: var(--font-size-base);
|
|
line-height: var(--line-height);
|
|
color: var(--color-text);
|
|
background: var(--color-bg);
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
/* === Typography === */
|
|
h1, h2, h3 { margin-bottom: var(--spacing-md); }
|
|
p { margin-bottom: var(--spacing-md); }
|
|
a { color: var(--color-link); }
|
|
code { font-family: var(--font-mono); }
|
|
|
|
/* === Layout === */
|
|
header, main, footer {
|
|
max-width: var(--max-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
margin-bottom: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
/* === Components === */
|
|
.note-list { /* ... */ }
|
|
.h-entry { /* ... */ }
|
|
.flash { /* ... */ }
|
|
.button { /* ... */ }
|
|
.form-group { /* ... */ }
|
|
|
|
/* === Dev Mode Warning === */
|
|
.dev-mode-warning {
|
|
background: var(--color-error);
|
|
color: white;
|
|
padding: var(--spacing-sm);
|
|
text-align: center;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* === Admin === */
|
|
.admin-nav { /* ... */ }
|
|
.note-table { /* ... */ }
|
|
|
|
/* === Responsive === */
|
|
@media (min-width: 768px) {
|
|
body { padding: var(--spacing-lg); }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## JavaScript Architecture
|
|
|
|
### File: `static/js/preview.js` (Optional)
|
|
|
|
**Purpose**: Live markdown preview in admin editor
|
|
|
|
**Implementation**:
|
|
```javascript
|
|
// Progressive enhancement - form works without this
|
|
(function() {
|
|
'use strict';
|
|
|
|
// Only run on note editor pages
|
|
const textarea = document.getElementById('note-content');
|
|
if (!textarea) return;
|
|
|
|
// Create preview container
|
|
const preview = document.createElement('div');
|
|
preview.id = 'markdown-preview';
|
|
preview.className = 'preview-panel';
|
|
textarea.parentNode.appendChild(preview);
|
|
|
|
// Use marked.js from CDN (loaded in template)
|
|
if (typeof marked === 'undefined') {
|
|
console.warn('Marked.js not loaded, preview disabled');
|
|
return;
|
|
}
|
|
|
|
// Update preview on input
|
|
let debounceTimer;
|
|
textarea.addEventListener('input', function() {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(function() {
|
|
preview.innerHTML = marked.parse(textarea.value);
|
|
}, 300);
|
|
});
|
|
|
|
// Initial render
|
|
preview.innerHTML = marked.parse(textarea.value);
|
|
})();
|
|
```
|
|
|
|
**Load in template** (optional):
|
|
```html
|
|
{% if request.path.startswith('/admin/') %}
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="{{ url_for('static', filename='js/preview.js') }}"></script>
|
|
{% endif %}
|
|
```
|
|
|
|
**Graceful degradation**: Form works without JavaScript
|
|
|
|
---
|
|
|
|
## Development Authentication Implementation
|
|
|
|
See ADR-011 for complete specification.
|
|
|
|
### Module: `starpunk/dev_auth.py`
|
|
|
|
**Functions**:
|
|
```python
|
|
def dev_login() -> Response:
|
|
"""Create development session instantly"""
|
|
|
|
def is_dev_mode() -> bool:
|
|
"""Check if development mode enabled"""
|
|
|
|
def validate_dev_config() -> None:
|
|
"""Validate development configuration"""
|
|
```
|
|
|
|
### Configuration Validation
|
|
|
|
```python
|
|
def validate_config(app):
|
|
"""Validate app configuration on startup"""
|
|
dev_mode = app.config.get('DEV_MODE', False)
|
|
|
|
if dev_mode:
|
|
# Warn prominently
|
|
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
|
|
if not app.config.get('DEV_ADMIN_ME'):
|
|
raise ConfigError("DEV_MODE requires DEV_ADMIN_ME")
|
|
|
|
# Register dev routes
|
|
register_dev_routes(app)
|
|
else:
|
|
# Production mode: require IndieLogin config
|
|
if not app.config.get('ADMIN_ME'):
|
|
raise ConfigError("Production mode requires ADMIN_ME")
|
|
```
|
|
|
|
### Visual Indicators
|
|
|
|
```html
|
|
<!-- All templates check for DEV_MODE -->
|
|
{% if config.DEV_MODE %}
|
|
<div class="dev-mode-warning">
|
|
⚠️ DEVELOPMENT MODE - Authentication bypassed
|
|
</div>
|
|
{% endif %}
|
|
```
|
|
|
|
---
|
|
|
|
## File Organization
|
|
|
|
```
|
|
starpunk/
|
|
├── __init__.py
|
|
├── auth.py # Production auth (existing)
|
|
├── dev_auth.py # Development auth (new)
|
|
├── config.py # Configuration (update)
|
|
├── database.py # Database access (existing)
|
|
├── models.py # Data models (existing)
|
|
├── notes.py # Note management (existing)
|
|
├── utils.py # Utilities (existing)
|
|
└── routes/
|
|
├── __init__.py # Route registration
|
|
├── public.py # Public routes (new)
|
|
├── admin.py # Admin routes (new)
|
|
├── auth.py # Auth routes (new)
|
|
└── dev_auth.py # Dev auth routes (new, conditional)
|
|
|
|
templates/
|
|
├── base.html # Public base template
|
|
├── index.html # Homepage
|
|
├── note.html # Note permalink
|
|
├── admin/
|
|
│ ├── base.html # Admin base template
|
|
│ ├── login.html # Login form
|
|
│ ├── dashboard.html # Admin dashboard
|
|
│ ├── new.html # Create note
|
|
│ └── edit.html # Edit note
|
|
└── dev/
|
|
└── login.html # Dev login (optional)
|
|
|
|
static/
|
|
├── css/
|
|
│ └── style.css # Single stylesheet
|
|
└── js/
|
|
└── preview.js # Markdown preview (optional)
|
|
|
|
tests/
|
|
├── test_routes_public.py # Public route tests (new)
|
|
├── test_routes_admin.py # Admin route tests (new)
|
|
└── test_dev_auth.py # Dev auth tests (new)
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Updates
|
|
|
|
### Environment Variables
|
|
|
|
```bash
|
|
# Existing
|
|
SITE_URL=https://starpunk.example.com
|
|
ADMIN_ME=https://yoursite.com
|
|
SESSION_SECRET=<random-secret>
|
|
DATA_PATH=./data
|
|
|
|
# New for Phase 4
|
|
DEV_MODE=false # Enable development authentication
|
|
DEV_ADMIN_ME= # Identity for dev mode (required if DEV_MODE=true)
|
|
VERSION=0.5.0 # Application version
|
|
```
|
|
|
|
### `starpunk/config.py` Updates
|
|
|
|
```python
|
|
class Config:
|
|
# Existing config...
|
|
|
|
# Development mode (default: false)
|
|
DEV_MODE = os.getenv('DEV_MODE', 'false').lower() == 'true'
|
|
DEV_ADMIN_ME = os.getenv('DEV_ADMIN_ME', '')
|
|
|
|
# Application metadata
|
|
VERSION = os.getenv('VERSION', '0.5.0')
|
|
|
|
@staticmethod
|
|
def validate():
|
|
"""Validate configuration on startup"""
|
|
if Config.DEV_MODE:
|
|
if not Config.DEV_ADMIN_ME:
|
|
raise ValueError("DEV_MODE requires DEV_ADMIN_ME")
|
|
# Log warning
|
|
else:
|
|
if not Config.ADMIN_ME:
|
|
raise ValueError("Production mode requires ADMIN_ME")
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Authentication
|
|
- All admin routes protected by `@require_auth`
|
|
- Session validation on every request
|
|
- HttpOnly, Secure cookies in production
|
|
- Dev auth clearly separated and guarded
|
|
|
|
### Input Validation
|
|
- Markdown content: no special validation (markdown is safe)
|
|
- Form inputs: Flask form validation
|
|
- Slugs: validated by notes module
|
|
- URLs: validated by auth module
|
|
|
|
### CSRF Protection
|
|
- State tokens for IndieAuth flow
|
|
- SameSite cookies
|
|
- POST for state-changing operations
|
|
|
|
### XSS Prevention
|
|
- Jinja2 auto-escaping enabled
|
|
- Markdown rendered to HTML server-side
|
|
- No user-generated HTML accepted
|
|
- CSP headers (future enhancement)
|
|
|
|
### Development Mode Security
|
|
- DEV_MODE must be explicitly enabled
|
|
- Returns 404 for dev routes when disabled
|
|
- Prominent warnings when enabled
|
|
- Configuration validation prevents accidents
|
|
- Cannot coexist with production URL
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
|
|
**Public Routes** (`tests/test_routes_public.py`):
|
|
- Test homepage renders
|
|
- Test note permalink
|
|
- Test 404 for unpublished notes
|
|
- Test microformats markup
|
|
|
|
**Admin Routes** (`tests/test_routes_admin.py`):
|
|
- Test login form renders
|
|
- Test dashboard requires auth
|
|
- Test create note flow
|
|
- Test edit note flow
|
|
- Test delete note flow
|
|
- Test logout
|
|
|
|
**Dev Auth** (`tests/test_dev_auth.py`):
|
|
- Test dev login creates session
|
|
- Test dev routes return 404 when disabled
|
|
- Test configuration validation
|
|
- Test dev mode indicators
|
|
|
|
### Integration Tests
|
|
- Full authentication flow (mocked IndieLogin)
|
|
- Create note end-to-end
|
|
- Edit note end-to-end
|
|
- Delete note end-to-end
|
|
- Dev auth flow
|
|
|
|
### Manual Tests
|
|
- Browser testing (Chrome, Firefox, Safari)
|
|
- Mobile responsive testing
|
|
- Microformats validation (indiewebify.me)
|
|
- Accessibility testing
|
|
- Real IndieLogin authentication
|
|
|
|
---
|
|
|
|
## Performance Considerations
|
|
|
|
### Response Times
|
|
- Homepage: < 200ms (database query + template render)
|
|
- Note page: < 200ms (database + file read + markdown render)
|
|
- Admin dashboard: < 200ms (database query + template)
|
|
|
|
### Optimizations
|
|
- Database indexes on notes.created_at
|
|
- Single query per page (minimize DB roundtrips)
|
|
- Optional: template caching (Flask-Caching)
|
|
- Static assets cached by browser
|
|
|
|
### Resource Usage
|
|
- Minimal memory (few templates in memory)
|
|
- No heavy JavaScript processing
|
|
- Server-side rendering reduces client load
|
|
|
|
---
|
|
|
|
## Migration from Previous Phases
|
|
|
|
### Database
|
|
- No schema changes needed
|
|
- All tables from Phase 3 remain
|
|
|
|
### Code Changes
|
|
- Add route modules (new files)
|
|
- Add templates (new files)
|
|
- Add CSS (new file)
|
|
- Add dev_auth module (new file)
|
|
- Update config.py
|
|
- Update app.py to register routes
|
|
|
|
### Configuration
|
|
- Add DEV_MODE and DEV_ADMIN_ME variables
|
|
- No breaking changes to existing config
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
Phase 4 is complete when:
|
|
|
|
### Functional Requirements
|
|
- [ ] Public homepage displays recent notes
|
|
- [ ] Note permalinks work correctly
|
|
- [ ] Admin login works (IndieLogin)
|
|
- [ ] Admin dashboard displays all notes
|
|
- [ ] Create note form works
|
|
- [ ] Edit note form works
|
|
- [ ] Delete note works with confirmation
|
|
- [ ] Logout works correctly
|
|
- [ ] Dev auth works for local testing
|
|
- [ ] Flash messages display correctly
|
|
|
|
### Security Requirements
|
|
- [ ] All admin routes require authentication
|
|
- [ ] Session cookies are secure
|
|
- [ ] Dev auth returns 404 when disabled
|
|
- [ ] Configuration validated on startup
|
|
- [ ] CSRF protection working
|
|
- [ ] No XSS vulnerabilities
|
|
|
|
### Quality Requirements
|
|
- [ ] Test coverage > 90%
|
|
- [ ] All tests pass
|
|
- [ ] No linting errors (flake8)
|
|
- [ ] Code formatted (black)
|
|
- [ ] Templates validated (HTML5)
|
|
- [ ] Microformats validated
|
|
|
|
### Documentation Requirements
|
|
- [ ] All routes documented
|
|
- [ ] Template structure documented
|
|
- [ ] Dev auth usage documented
|
|
- [ ] Security considerations documented
|
|
- [ ] Configuration documented
|
|
|
|
### Performance Requirements
|
|
- [ ] Homepage loads < 200ms
|
|
- [ ] Note pages load < 200ms
|
|
- [ ] Admin pages load < 200ms
|
|
- [ ] Forms submit < 100ms
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Phase 4.1: Core Routes and Templates
|
|
- [ ] Create route modules (public, admin, auth)
|
|
- [ ] Implement public routes (/, /note/<slug>)
|
|
- [ ] Implement auth routes (login, callback, logout)
|
|
- [ ] Implement admin routes (dashboard, new, edit, delete)
|
|
- [ ] Create base templates (base.html, admin/base.html)
|
|
- [ ] Create public templates (index.html, note.html)
|
|
- [ ] Create admin templates (login, dashboard, new, edit)
|
|
- [ ] Test route functionality
|
|
|
|
### Phase 4.2: Development Authentication
|
|
- [ ] Create dev_auth.py module
|
|
- [ ] Implement dev login route
|
|
- [ ] Add configuration validation
|
|
- [ ] Add dev mode warnings
|
|
- [ ] Add visual indicators
|
|
- [ ] Test dev auth functionality
|
|
- [ ] Document dev auth usage
|
|
|
|
### Phase 4.3: Frontend Styling
|
|
- [ ] Create style.css with variables
|
|
- [ ] Style public interface
|
|
- [ ] Style admin interface
|
|
- [ ] Add responsive breakpoints
|
|
- [ ] Style flash messages
|
|
- [ ] Style forms and buttons
|
|
- [ ] Test on mobile devices
|
|
|
|
### Phase 4.4: JavaScript Enhancements (Optional)
|
|
- [ ] Add marked.js preview (optional)
|
|
- [ ] Implement preview.js
|
|
- [ ] Test progressive enhancement
|
|
- [ ] Ensure graceful degradation
|
|
|
|
### Phase 4.5: Testing and Validation
|
|
- [ ] Write route unit tests
|
|
- [ ] Write integration tests
|
|
- [ ] Write dev auth tests
|
|
- [ ] Validate microformats
|
|
- [ ] Validate HTML5
|
|
- [ ] Test accessibility
|
|
- [ ] Test real IndieLogin auth
|
|
- [ ] Security audit
|
|
|
|
### Phase 4.6: Documentation
|
|
- [ ] Document routes
|
|
- [ ] Document templates
|
|
- [ ] Document CSS architecture
|
|
- [ ] Document dev auth
|
|
- [ ] Update CHANGELOG
|
|
- [ ] Increment version to 0.5.0
|
|
|
|
---
|
|
|
|
## Success Metrics
|
|
|
|
Phase 4 is successful if:
|
|
|
|
1. **User can view published notes** on public interface
|
|
2. **User can authenticate** via IndieLogin
|
|
3. **Admin can create notes** via web interface
|
|
4. **Admin can edit notes** via web interface
|
|
5. **Admin can delete notes** via web interface
|
|
6. **Developer can test locally** without deployment
|
|
7. **All routes are secure** and properly protected
|
|
8. **Templates render correctly** with microformats
|
|
9. **Tests pass** with >90% coverage
|
|
10. **Dev auth is safe** and clearly separated
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
### Python Packages (Existing)
|
|
- Flask (web framework)
|
|
- Jinja2 (templating, included with Flask)
|
|
- markdown (markdown rendering, existing)
|
|
- httpx (HTTP client, existing)
|
|
|
|
### No New Dependencies Required
|
|
|
|
### External Services
|
|
- IndieLogin.com (production auth)
|
|
- None for dev mode
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
### Technical Risks
|
|
|
|
**Risk: Dev auth accidentally enabled in production**
|
|
- Likelihood: Medium
|
|
- Impact: Critical (authentication bypass)
|
|
- Mitigation: Configuration validation, startup warnings, visual indicators, documentation
|
|
|
|
**Risk: Template XSS vulnerabilities**
|
|
- Likelihood: Low
|
|
- Impact: High
|
|
- Mitigation: Jinja2 auto-escaping, no user HTML, code review
|
|
|
|
**Risk: Session cookie theft**
|
|
- Likelihood: Low
|
|
- Impact: High
|
|
- Mitigation: HttpOnly, Secure, SameSite cookies, HTTPS required
|
|
|
|
### Operational Risks
|
|
|
|
**Risk: User forgets to disable DEV_MODE**
|
|
- Likelihood: Medium
|
|
- Impact: Critical
|
|
- Mitigation: Deployment checklist, visual warnings, config validation
|
|
|
|
**Risk: Missing IndieLogin configuration**
|
|
- Likelihood: Low
|
|
- Impact: Medium
|
|
- Mitigation: Startup validation, clear error messages, documentation
|
|
|
|
---
|
|
|
|
## Future Enhancements (V2+)
|
|
|
|
Deferred features:
|
|
- Advanced note editor (WYSIWYG)
|
|
- Image uploads
|
|
- Draft saving (auto-save)
|
|
- Note preview before publish
|
|
- Bulk operations
|
|
- Search functionality
|
|
- Tags UI
|
|
- Custom themes
|
|
- Admin user management
|
|
- Activity log
|
|
- Session management UI
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
### Internal Documentation
|
|
- [ADR-003: Frontend Technology](/home/phil/Projects/starpunk/docs/decisions/ADR-003-frontend-technology.md)
|
|
- [ADR-005: IndieLogin Authentication](/home/phil/Projects/starpunk/docs/decisions/ADR-005-indielogin-authentication.md)
|
|
- [ADR-010: Authentication Module Design](/home/phil/Projects/starpunk/docs/decisions/ADR-010-authentication-module-design.md)
|
|
- [ADR-011: Development Authentication Mechanism](/home/phil/Projects/starpunk/docs/decisions/ADR-011-development-authentication-mechanism.md)
|
|
- [Architecture Overview](/home/phil/Projects/starpunk/docs/architecture/overview.md)
|
|
- [Phase 3 Report](/home/phil/Projects/starpunk/docs/reports/phase-3-authentication-20251118.md)
|
|
|
|
### External Standards
|
|
- [Microformats2 h-entry](http://microformats.org/wiki/h-entry)
|
|
- [HTML5 Specification](https://html.spec.whatwg.org/)
|
|
- [Flask Documentation](https://flask.palletsprojects.com/)
|
|
- [Jinja2 Documentation](https://jinja.palletsprojects.com/)
|
|
- [OWASP Web Security](https://owasp.org/www-project-web-security-testing-guide/)
|
|
|
|
---
|
|
|
|
**Phase**: 4
|
|
**Version Target**: 0.5.0
|
|
**Status**: Design Complete, Ready for Implementation
|
|
**Next Phase**: Phase 5 (RSS Feed Generation)
|