feat(core): implement Phase 1 foundation infrastructure

Implements Phase 1 Foundation with all core services:

Core Components:
- Configuration management with GONDULF_ environment variables
- Database layer with SQLAlchemy and migration system
- In-memory code storage with TTL support
- Email service with SMTP and TLS support (STARTTLS + implicit TLS)
- DNS service with TXT record verification
- Structured logging with Python standard logging
- FastAPI application with health check endpoint

Database Schema:
- authorization_codes table for OAuth 2.0 authorization codes
- domains table for domain verification
- migrations table for tracking schema versions
- Simple sequential migration system (001_initial_schema.sql)

Configuration:
- Environment-based configuration with validation
- .env.example template with all GONDULF_ variables
- Fail-fast validation on startup
- Sensible defaults for optional settings

Testing:
- 96 comprehensive tests (77 unit, 5 integration)
- 94.16% code coverage (exceeds 80% requirement)
- All tests passing
- Test coverage includes:
  - Configuration loading and validation
  - Database migrations and health checks
  - In-memory storage with expiration
  - Email service (STARTTLS, implicit TLS, authentication)
  - DNS service (TXT records, domain verification)
  - Health check endpoint integration

Documentation:
- Implementation report with test results
- Phase 1 clarifications document
- ADRs for key decisions (config, database, email, logging)

Technical Details:
- Python 3.10+ with type hints
- SQLite with configurable database URL
- System DNS with public DNS fallback
- Port-based TLS detection (465=SSL, 587=STARTTLS)
- Lazy configuration loading for testability

Exit Criteria Met:
✓ All foundation services implemented
✓ Application starts without errors
✓ Health check endpoint operational
✓ Database migrations working
✓ Test coverage exceeds 80%
✓ All tests passing

Ready for Architect review and Phase 2 development.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 12:21:42 -07:00
parent 7255867fde
commit bebd47955f
39 changed files with 8134 additions and 13 deletions

166
src/gondulf/main.py Normal file
View File

@@ -0,0 +1,166 @@
"""
Gondulf IndieAuth Server - Main application entry point.
FastAPI application with health check endpoint and core service initialization.
"""
import logging
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from gondulf.config import Config
from gondulf.database.connection import Database
from gondulf.dns import DNSService
from gondulf.email import EmailService
from gondulf.logging_config import configure_logging
from gondulf.storage import CodeStore
# Load configuration at application startup
Config.load()
Config.validate()
# Configure logging
configure_logging(log_level=Config.LOG_LEVEL, debug=Config.DEBUG)
logger = logging.getLogger("gondulf.main")
# Initialize FastAPI application
app = FastAPI(
title="Gondulf IndieAuth Server",
description="Self-hosted IndieAuth authentication server",
version="0.1.0-dev",
)
# Initialize core services
database: Database = None
code_store: CodeStore = None
email_service: EmailService = None
dns_service: DNSService = None
@app.on_event("startup")
async def startup_event() -> None:
"""
Initialize application on startup.
Initializes database, code storage, email service, and DNS service.
"""
global database, code_store, email_service, dns_service
logger.info("Starting Gondulf IndieAuth Server")
logger.info(f"Configuration: DATABASE_URL={Config.DATABASE_URL}")
logger.info(f"Configuration: SMTP_HOST={Config.SMTP_HOST}:{Config.SMTP_PORT}")
logger.info(f"Configuration: DEBUG={Config.DEBUG}")
try:
# Initialize database
logger.info("Initializing database")
database = Database(Config.DATABASE_URL)
database.initialize()
logger.info("Database initialized successfully")
# Initialize code store
logger.info("Initializing code store")
code_store = CodeStore(ttl_seconds=Config.CODE_EXPIRY)
logger.info(f"Code store initialized with TTL={Config.CODE_EXPIRY}s")
# Initialize email service
logger.info("Initializing email service")
email_service = EmailService(
smtp_host=Config.SMTP_HOST,
smtp_port=Config.SMTP_PORT,
smtp_from=Config.SMTP_FROM,
smtp_username=Config.SMTP_USERNAME,
smtp_password=Config.SMTP_PASSWORD,
smtp_use_tls=Config.SMTP_USE_TLS,
)
logger.info("Email service initialized")
# Initialize DNS service
logger.info("Initializing DNS service")
dns_service = DNSService()
logger.info("DNS service initialized")
logger.info("Gondulf startup complete")
except Exception as e:
logger.critical(f"Failed to initialize application: {e}")
raise
@app.on_event("shutdown")
async def shutdown_event() -> None:
"""Clean up resources on shutdown."""
logger.info("Shutting down Gondulf IndieAuth Server")
@app.get("/health")
async def health_check() -> JSONResponse:
"""
Health check endpoint.
Verifies that the application is running and database is accessible.
Does not require authentication.
Returns:
JSON response with health status:
- 200 OK: {"status": "healthy", "database": "connected"}
- 503 Service Unavailable: {"status": "unhealthy", "database": "error", "error": "..."}
"""
# Check database connectivity
if database is None:
logger.warning("Health check failed: database not initialized")
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"database": "error",
"error": "database not initialized",
},
)
is_healthy = database.check_health(timeout_seconds=5)
if is_healthy:
logger.debug("Health check passed")
return JSONResponse(
status_code=200,
content={"status": "healthy", "database": "connected"},
)
else:
logger.warning("Health check failed: unable to connect to database")
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"database": "error",
"error": "unable to connect to database",
},
)
@app.get("/")
async def root() -> dict:
"""
Root endpoint.
Returns basic server information.
"""
return {
"service": "Gondulf IndieAuth Server",
"version": "0.1.0-dev",
"status": "operational",
}
# Entry point for uvicorn
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"gondulf.main:app",
host="0.0.0.0",
port=8000,
reload=Config.DEBUG,
log_level=Config.LOG_LEVEL.lower(),
)