Files
StarPunk/starpunk/database.py
Phil Skentelbery d4f1bfb198 feat: Implement Phase 3 authentication module with IndieLogin support
Implement complete authentication system following ADR-010 and Phase 3 design specs.
This is a MINOR version increment (0.3.0 -> 0.4.0) as it adds new functionality.

Authentication Features:
- IndieLogin authentication flow via indielogin.com
- Secure session management with SHA-256 token hashing
- CSRF protection with single-use state tokens
- Session lifecycle (create, verify, destroy)
- require_auth decorator for protected routes
- Automatic cleanup of expired sessions
- IP address and user agent tracking

Security Measures:
- Cryptographically secure token generation (secrets module)
- Token hashing for storage (never plaintext)
- SQL injection prevention (prepared statements)
- Single-use CSRF state tokens
- 30-day session expiry with activity refresh
- Comprehensive security logging

Implementation Details:
- starpunk/auth.py: 406 lines, 6 core functions, 4 helpers, 4 exceptions
- tests/test_auth.py: 648 lines, 37 tests, 96% coverage
- Database schema updates for sessions and auth_state tables
- URL validation utility added to utils.py

Test Coverage:
- 37 authentication tests
- 96% code coverage (exceeds 90% target)
- All security features tested
- Edge cases and error paths covered

Documentation:
- Implementation report in docs/reports/
- Updated CHANGELOG.md with detailed changes
- Version incremented to 0.4.0
- ADR-010 and Phase 3 design docs included

Follows project standards:
- Black code formatting (88 char lines)
- Flake8 linting (no errors)
- Python coding standards
- Type hints on all functions
- Comprehensive docstrings

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 20:35:36 -07:00

109 lines
2.9 KiB
Python

"""
Database initialization and operations for StarPunk
SQLite database for metadata, sessions, and tokens
"""
import sqlite3
from pathlib import Path
# Database schema
SCHEMA_SQL = """
-- Notes metadata (content is in files)
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
file_path TEXT UNIQUE NOT NULL,
published BOOLEAN DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
content_hash TEXT
);
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notes_published ON notes(published);
CREATE INDEX IF NOT EXISTS idx_notes_slug ON notes(slug);
CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at);
-- Authentication sessions (IndieLogin)
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_token_hash TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP,
user_agent TEXT,
ip_address TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(session_token_hash);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_me ON sessions(me);
-- Micropub access tokens
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
me TEXT NOT NULL,
client_id TEXT,
scope TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tokens_me ON tokens(me);
-- CSRF state tokens (for IndieAuth flow)
CREATE TABLE IF NOT EXISTS auth_state (
state TEXT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
redirect_uri TEXT
);
CREATE INDEX IF NOT EXISTS idx_auth_state_expires ON auth_state(expires_at);
"""
def init_db(app=None):
"""
Initialize database schema
Args:
app: Flask application instance (optional, for config access)
"""
if app:
db_path = app.config["DATABASE_PATH"]
else:
# Fallback to default path
db_path = Path("./data/starpunk.db")
# Ensure parent directory exists
db_path.parent.mkdir(parents=True, exist_ok=True)
# Create database and schema
conn = sqlite3.connect(db_path)
try:
conn.executescript(SCHEMA_SQL)
conn.commit()
print(f"Database initialized: {db_path}")
finally:
conn.close()
def get_db(app):
"""
Get database connection
Args:
app: Flask application instance
Returns:
sqlite3.Connection
"""
db_path = app.config["DATABASE_PATH"]
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row # Return rows as dictionaries
return conn