Phase 3 Implementation: - Token service with secure token generation and validation - Token endpoint (POST /token) with OAuth 2.0 compliance - Database migration 003 for tokens table - Authorization code validation and single-use enforcement Phase 1 Updates: - Enhanced CodeStore to support dict values with JSON serialization - Maintains backward compatibility Phase 2 Updates: - Authorization codes now include PKCE fields, used flag, timestamps - Complete metadata structure for token exchange Security: - 256-bit cryptographically secure tokens (secrets.token_urlsafe) - SHA-256 hashed storage (no plaintext) - Constant-time comparison for validation - Single-use code enforcement with replay detection Testing: - 226 tests passing (100%) - 87.27% coverage (exceeds 80% requirement) - OAuth 2.0 compliance verified This completes the v1.0.0 MVP with full IndieAuth authorization code flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
173 lines
4.9 KiB
Python
173 lines
4.9 KiB
Python
"""
|
|
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.routers import authorization, token, verification
|
|
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",
|
|
)
|
|
|
|
# Register routers
|
|
app.include_router(authorization.router)
|
|
app.include_router(token.router)
|
|
app.include_router(verification.router)
|
|
|
|
# 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(),
|
|
)
|