Stream D: Security hardening — 10 fixes from QA review
- SEC-05: Fail fast if SECRET_KEY is default 'change-me-in-production' - SEC-06: CORS allow_credentials=False (Bearer tokens don't need it) - SEC-08: Input validation on auth schemas (min/max length, username pattern) - SEC-10: .dockerignore files (root, backend, mcp-connector) - SEC-11: Hardcode ALGORITHM='HS256' as constant, remove from settings - CQ-01: Gate crawl scheduler on CRAWL_ENABLED env var (default false) - OS-02: CONTRIBUTING.md - OS-03: CHANGELOG.md with v1.0.0 entry - setup.ps1: Add API_TOKEN generation (Windows parity with setup.sh)
This commit is contained in:
parent
c075eb9257
commit
6e082ce58f
10 changed files with 113 additions and 20 deletions
|
|
@ -1,7 +1,11 @@
|
||||||
.git
|
.git
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
content/
|
content/
|
||||||
*.md
|
node_modules/
|
||||||
LICENSE
|
dist/
|
||||||
setup.sh
|
*.log
|
||||||
setup.ps1
|
.DS_Store
|
||||||
|
|
|
||||||
27
CHANGELOG.md
Normal file
27
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Diligence are documented here.
|
||||||
|
|
||||||
|
## [1.0.0] — 2026-06-18
|
||||||
|
|
||||||
|
Initial open-source release.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Points economy with daily gate, weekly targets, weekly reset
|
||||||
|
- Science-based onboarding (PAR-Q+, TTM, BREQ-2 motivation profiling)
|
||||||
|
- Activity logging (manual + Strava OAuth + Polar OAuth sync)
|
||||||
|
- Food logging with Open Food Facts barcode scanning + USDA FoodData Central
|
||||||
|
- 90-day structured program tracking
|
||||||
|
- Configurable reward shop
|
||||||
|
- In-app support chat with Telegram notifications
|
||||||
|
- 14-tool MCP connector for AI agent integration
|
||||||
|
- Meal plan system with compliance tracking
|
||||||
|
- Dynamic integration configuration (11 providers)
|
||||||
|
- B2A discovery layer (llms.txt, agent-card.json, SKILL.md)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Fernet-encrypted credential storage (HKDF key derivation)
|
||||||
|
- Signed JWT OAuth state parameters
|
||||||
|
- MCP connector authentication via API_TOKEN
|
||||||
|
- First-user auto-admin grant
|
||||||
|
- Traceback suppression in error responses
|
||||||
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Contributing to Diligence
|
||||||
|
|
||||||
|
Thanks for your interest in contributing!
|
||||||
|
|
||||||
|
## How to contribute
|
||||||
|
|
||||||
|
1. **Open an issue** to discuss the change before starting work
|
||||||
|
2. **Fork the repo** and create a branch from `main`
|
||||||
|
3. **Test locally**: `docker compose up -d` and verify all 4 containers start healthy
|
||||||
|
4. **Submit a PR** with a clear description of what changed and why
|
||||||
|
|
||||||
|
## Development setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DiligenceWorks/Diligence.git
|
||||||
|
cd Diligence
|
||||||
|
./setup.sh # Mac/Linux
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost and register. First user gets admin automatically.
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- Backend: Python 3.11+, FastAPI, async/await, type hints
|
||||||
|
- Frontend: React with hooks, no class components
|
||||||
|
- CSS: use CSS variables from `index.css`, never hardcode colors
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
Open a GitHub Discussion or reach out at hello@diligenceworks.online.
|
||||||
11
backend/.dockerignore
Normal file
11
backend/.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
content/
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
@ -10,7 +10,7 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
secret_key: str = "change-me-in-production"
|
secret_key: str = "change-me-in-production"
|
||||||
algorithm: str = "HS256"
|
crawl_enabled: bool = False # Set CRAWL_ENABLED=true to start program crawl scheduler
|
||||||
access_token_expire_minutes: int = 1440 # 24 hours
|
access_token_expire_minutes: int = 1440 # 24 hours
|
||||||
api_token: str = "" # MCP connector auth — generated by setup.sh
|
api_token: str = "" # MCP connector auth — generated by setup.sh
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
@ -27,6 +28,13 @@ async def lifespan(app: FastAPI):
|
||||||
logger.error("Could not initialize database after 10 attempts — starting without tables")
|
logger.error("Could not initialize database after 10 attempts — starting without tables")
|
||||||
|
|
||||||
|
|
||||||
|
# SEC-05: Fail fast if SECRET_KEY not configured
|
||||||
|
from app.config import get_settings
|
||||||
|
_s = get_settings()
|
||||||
|
if _s.secret_key == "change-me-in-production":
|
||||||
|
logger.error("CRITICAL: SECRET_KEY not set. Run ./setup.sh or set SECRET_KEY in .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
# Run lightweight migrations for schema changes
|
# Run lightweight migrations for schema changes
|
||||||
try:
|
try:
|
||||||
await run_migrations()
|
await run_migrations()
|
||||||
|
|
@ -41,14 +49,17 @@ async def lifespan(app: FastAPI):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Resource seeding failed (non-fatal): {e}")
|
logger.warning(f"Resource seeding failed (non-fatal): {e}")
|
||||||
|
|
||||||
# Start background crawl queue scheduler
|
# Start background crawl queue scheduler (gated on CRAWL_ENABLED)
|
||||||
crawl_task = None
|
crawl_task = None
|
||||||
|
if _s.crawl_enabled:
|
||||||
try:
|
try:
|
||||||
from app.services.crawl_scheduler import crawl_queue_loop
|
from app.services.crawl_scheduler import crawl_queue_loop
|
||||||
crawl_task = asyncio.create_task(crawl_queue_loop())
|
crawl_task = asyncio.create_task(crawl_queue_loop())
|
||||||
logger.info("Crawl queue scheduler started")
|
logger.info("Crawl queue scheduler started")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}")
|
logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}")
|
||||||
|
else:
|
||||||
|
logger.info("Crawl scheduler disabled (set CRAWL_ENABLED=true to enable)")
|
||||||
|
|
||||||
logger.info("Fitness Rewards backend started")
|
logger.info("Fitness Rewards backend started")
|
||||||
yield
|
yield
|
||||||
|
|
@ -68,7 +79,7 @@ app = FastAPI(title="Fitness Rewards", version="1.0.0", lifespan=lifespan)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=False # Bearer tokens don't need credentials mode,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
username: str
|
username: str = Field(min_length=3, max_length=50)
|
||||||
password: str
|
password: str = Field(min_length=8, max_length=128)
|
||||||
|
|
||||||
|
|
||||||
class RegisterRequest(BaseModel):
|
class RegisterRequest(BaseModel):
|
||||||
username: str
|
username: str = Field(min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$')
|
||||||
password: str
|
password: str = Field(min_length=8, max_length=128)
|
||||||
display_name: str
|
display_name: str = Field(min_length=1, max_length=100)
|
||||||
email: str | None = None
|
email: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from app.config import get_settings
|
||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
ALGORITHM = "HS256" # Hardcoded — not configurable for security
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ def verify_password(plain: str, hashed: str) -> bool:
|
||||||
def create_access_token(user_id: str, expires_delta: timedelta | None = None) -> str:
|
def create_access_token(user_id: str, expires_delta: timedelta | None = None) -> str:
|
||||||
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes))
|
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes))
|
||||||
payload = {"sub": user_id, "exp": expire}
|
payload = {"sub": user_id, "exp": expire}
|
||||||
return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
|
return jwt.encode(payload, settings.secret_key, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
|
|
|
||||||
4
mcp-connector/.dockerignore
Normal file
4
mcp-connector/.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
# Diligence — Windows Setup
|
# Diligence — Windows Setup
|
||||||
|
# Generate API_TOKEN for MCP connector auth
|
||||||
|
$apiToken = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_})
|
||||||
|
(Get-Content .env) -replace '^API_TOKEN=.*', "API_TOKEN=$apiToken" | Set-Content .env
|
||||||
|
|
||||||
Write-Host "`n`e[36m💪 Diligence — Setup`e[0m`n"
|
Write-Host "`n`e[36m💪 Diligence — Setup`e[0m`n"
|
||||||
|
|
||||||
if (Test-Path .env) {
|
if (Test-Path .env) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue