Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e1a0335
Merge pull request #3 from Climate-Vision/feature/api-middleware-audit
Goldokpa Mar 28, 2026
4bddcb3
Merge pull request #4 from Climate-Vision/feature/analytics-statistics
Goldokpa Mar 28, 2026
cea2e6a
docs: add Francis Umo role documentation
Mar 31, 2026
d37cbe7
docs: add Olufemi Taiwo role documentation
Mar 31, 2026
319a88f
Merge pull request #5 from Climate-Vision/docs/francis-role-document
Goldokpa Mar 31, 2026
326f3eb
Merge pull request #6 from Climate-Vision/docs/femi-role-document
Goldokpa Mar 31, 2026
0f4a362
feat(data): GEE tile downloader, band mapping, and cloud masking (#7)
Oshgig Apr 15, 2026
51edfac
feat(inference): make pipeline analysis-aware with dynamic model load…
Oshgig Apr 15, 2026
f2b9373
Merge develop into main: data pipeline + analysis-aware inference
femi23 Apr 15, 2026
1257e7a
feat(api): enforce API key auth with dev bypass, surface is_synthetic…
femi23 Apr 19, 2026
256fbf6
ci: add pytest scaffolding and GitHub Actions workflow
Apr 19, 2026
139ed61
test(models): add UNet and Siamese architecture tests
Godswill-code Apr 19, 2026
0da6c79
docs: add first-time and intermediate contributor issue guides
Goldokpa Apr 19, 2026
ff21090
fix(frontend): correct case-sensitive import paths for Map components
Apr 19, 2026
cf96100
fix(pipeline): remove unnecessary global declaration causing flake8 F824
femi23 Apr 19, 2026
c3d02c1
ci: install system deps before pip install (GDAL, OpenGL)
Apr 19, 2026
f7a7564
ci: remove redundant gdal pip package and simplify system deps
Apr 19, 2026
7c317df
ci: install package in editable mode for pytest
Apr 19, 2026
b8e34ea
feat(data): add dataset, augmentation, and synthetic data modules
Apr 19, 2026
aa643ea
fix(deps): add email-validator for pydantic EmailStr support
Apr 19, 2026
6ac29d1
docs: update Victor's role doc with sprint progress and live CI config
Apr 19, 2026
9f58a51
fix: validate start_date <= end_date in PredictRequest with 422 response
kriti2110 May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgl1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -e .

- name: Lint with flake8
run: |
flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Test with pytest
run: |
pytest tests/ -v --tb=short

frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Type check and build
run: npm run build
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ ENV/
!notebooks/*.ipynb

# Data
data/
datasets/
/data/
/datasets/
*.tif
*.tiff
*.h5
Expand Down
28 changes: 27 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,33 @@ We are committed to providing a welcoming and inclusive environment. Please be r

#### First Time Contributors

Look for issues labeled `good first issue` - these are specifically chosen for newcomers.
Look for issues labeled `good first issue` — these are specifically chosen for newcomers.

**Recommended first issues (ready to pick up):**

| Issue | What You'll Learn | Time Estimate |
|-------|-----------------|---------------|
| [#9: Add frontend unit tests](https://github.com/Climate-Vision/ClimateVision/issues/9) | Vitest, React Testing Library, Vite | 2–4 hours |
| [#13: Add Docker Compose](https://github.com/Climate-Vision/ClimateVision/issues/13) | Docker, multi-service orchestration | 3–6 hours |

**How to claim an issue:**
1. Read the issue description and acceptance criteria
2. Comment "I'd like to work on this" — a maintainer will assign you
3. Fork the repo and create a branch: `git checkout -b feature/issue-9-frontend-tests`
4. Open a **draft PR** within 48 hours (even if incomplete) so we can give early feedback

**Need help?** Tag `@Climate-Vision/maintainers` in the issue or open a [Discussion](https://github.com/Climate-Vision/ClimateVision/discussions).

#### Intermediate Contributors

Ready for something meatier? These issues close critical gaps in our production pipeline:

| Issue | Area | Skills You'll Build |
|-------|------|-------------------|
| [#10: Alert delivery worker](https://github.com/Climate-Vision/ClimateVision/issues/10) | Backend | FastAPI BackgroundTasks, SMTP, webhooks |
| [#11: WebSocket real-time updates](https://github.com/Climate-Vision/ClimateVision/issues/11) | Full-stack | FastAPI WebSockets, React hooks, graceful degradation |
| [#12: ONNX Runtime inference](https://github.com/Climate-Vision/ClimateVision/issues/12) | MLOps | ONNX Runtime, PyTorch export, latency benchmarking |
| [#14: Carbon analytics API](https://github.com/Climate-Vision/ClimateVision/issues/14) | Analytics | Feature flags, API schema design, geospatial math |

#### Development Process

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/NewAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
import { Loader2 } from 'lucide-react'
import type { AnalysisType } from '../api'
import { predictJson } from '../api'
import { MapBBoxPicker } from '../components/map/MapBBoxPicker'
import { MapBBoxPicker } from '../components/Map/MapBBoxPicker'
import { AnalysisTypeSelector } from '../components/ui/AnalysisTypeSelector'
import { ResultsPanel } from '../components/results/ResultsPanel'
import { ErrorBoundary } from '../components/ui/ErrorBoundary'
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CloudUpload, FileText, X, ChevronDown, ChevronUp, Loader2 } from 'lucid
import type { AnalysisType } from '../api'
import { predictUpload } from '../api'
import { AnalysisTypeSelector } from '../components/ui/AnalysisTypeSelector'
import { MapBBoxPicker } from '../components/map/MapBBoxPicker'
import { MapBBoxPicker } from '../components/Map/MapBBoxPicker'
import { ErrorBoundary } from '../components/ui/ErrorBoundary'
import { useToast } from '../contexts/ToastContext'
import { useApp } from '../contexts/AppContext'
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ scikit-learn>=1.0.0

# Geospatial Data Processing
rasterio>=1.3.0
gdal>=3.4.0
geopandas>=0.12.0
shapely>=2.0.0
pyproj>=3.4.0
Expand Down Expand Up @@ -40,6 +39,7 @@ dask[complete]>=2023.1.0
fastapi>=0.95.0
uvicorn[standard]>=0.20.0
pydantic>=2.0.0
email-validator>=2.0.0
python-multipart>=0.0.5

# MLOps (optional)
Expand Down
206 changes: 206 additions & 0 deletions src/climatevision/api/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"""
API Key Authentication for ClimateVision API.

Provides secure API key validation and organization-based
access control for all protected endpoints.
"""

from __future__ import annotations

import hashlib
import hmac
import logging
import secrets
from datetime import datetime
from typing import Optional

from fastapi import HTTPException, Request, Security
from fastapi.security import APIKeyHeader

logger = logging.getLogger(__name__)

API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)


class APIKeyAuth:
"""
API Key authentication handler with organization context.

Validates API keys and extracts organization information
for request-scoped access control.
"""

def __init__(self, db_connection=None):
self._db = db_connection
self._key_cache: dict[str, dict] = {}

def generate_api_key(self, org_id: int, org_name: str) -> str:
"""
Generate a new API key for an organization.

Args:
org_id: Organization ID
org_name: Organization name

Returns:
New API key string (prefix + random bytes)
"""
prefix = "cv_"
random_part = secrets.token_urlsafe(32)
api_key = f"{prefix}{random_part}"

logger.info(
"api_key_generated",
extra={
"org_id": org_id,
"org_name": org_name,
"key_prefix": api_key[:8],
}
)

return api_key

def hash_key(self, api_key: str) -> str:
"""Hash an API key for secure storage."""
return hashlib.sha256(api_key.encode()).hexdigest()

def validate_key(self, api_key: str) -> Optional[dict]:
"""
Validate an API key and return organization context.

Args:
api_key: The API key to validate

Returns:
Organization dict if valid, None otherwise
"""
if not api_key or not api_key.startswith("cv_"):
return None

# Development bypass — allow cv_dev for local testing
if api_key == "cv_dev":
return {
"id": 0,
"name": "Development",
"demo": True,
}

# Check cache first
key_hash = self.hash_key(api_key)
if key_hash in self._key_cache:
cached = self._key_cache[key_hash]
if cached.get("expires_at", datetime.max) > datetime.utcnow():
return cached.get("org")

# Would query database in production
# For now, return None to indicate key not found
return None

def revoke_key(self, api_key: str) -> bool:
"""
Revoke an API key.

Args:
api_key: The API key to revoke

Returns:
True if revoked successfully
"""
key_hash = self.hash_key(api_key)

if key_hash in self._key_cache:
del self._key_cache[key_hash]

logger.info(
"api_key_revoked",
extra={"key_prefix": api_key[:8] if api_key else "unknown"}
)

return True


# Singleton instance
_auth_handler: Optional[APIKeyAuth] = None


def get_auth_handler() -> APIKeyAuth:
"""Get or create the API key auth handler."""
global _auth_handler
if _auth_handler is None:
_auth_handler = APIKeyAuth()
return _auth_handler


async def require_api_key(
request: Request,
api_key: Optional[str] = Security(API_KEY_HEADER)
) -> dict:
"""
FastAPI dependency for requiring API key authentication.

Usage:
@app.get("/protected")
async def protected_endpoint(org: dict = Depends(require_api_key)):
return {"org_id": org["id"]}
"""
if not api_key:
logger.warning(
"auth_failed",
extra={
"reason": "missing_api_key",
"path": request.url.path,
"client_ip": request.client.host if request.client else "unknown",
}
)
raise HTTPException(
status_code=401,
detail="API key required. Include X-API-Key header."
)

auth = get_auth_handler()
org = auth.validate_key(api_key)

if not org:
logger.warning(
"auth_failed",
extra={
"reason": "invalid_api_key",
"key_prefix": api_key[:8] if len(api_key) >= 8 else "short",
"path": request.url.path,
}
)
raise HTTPException(
status_code=401,
detail="Invalid API key."
)

# Attach org context to request state
request.state.organization = org

logger.info(
"auth_success",
extra={
"org_id": org.get("id"),
"org_name": org.get("name"),
"path": request.url.path,
}
)

return org


async def optional_api_key(
request: Request,
api_key: Optional[str] = Security(API_KEY_HEADER)
) -> Optional[dict]:
"""
FastAPI dependency for optional API key authentication.

Returns organization context if valid key provided, None otherwise.
Does not raise exceptions for missing/invalid keys.
"""
if not api_key:
return None

auth = get_auth_handler()
return auth.validate_key(api_key)
Loading
Loading