diff --git a/modern/.github/workflows/ci.yml b/modern/.github/workflows/ci.yml index 2febfc2..d014e38 100644 --- a/modern/.github/workflows/ci.yml +++ b/modern/.github/workflows/ci.yml @@ -6,6 +6,8 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check Python syntax - run: python -m py_compile modern/backend/server.py - - name: Check frontend package.json - run: cat modern/frontend/package.json + run: python -m py_compile modern/backend/*.py + - name: Run tests + run: | + python -m pip install -r modern/backend/requirements_full.txt + pytest -q diff --git a/modern/backend/app.py b/modern/backend/app.py index 28e6c11..ee31530 100644 --- a/modern/backend/app.py +++ b/modern/backend/app.py @@ -18,6 +18,18 @@ app.add_middleware( allow_headers=["*"], ) + +# HTTPS enforcement middleware (for production behind a proxy, check X-Forwarded-Proto) +@app.middleware('http') +async def https_redirect(request, call_next): + if os.getenv('FORCE_HTTPS', 'false').lower() == 'true': + proto = request.headers.get('x-forwarded-proto', request.url.scheme) + if proto != 'https': + from starlette.responses import RedirectResponse + url = request.url.replace(scheme='https') + return RedirectResponse(str(url)) + return await call_next(request) + @app.on_event('startup') def startup_event(): models.init_db() @@ -198,13 +210,16 @@ def list_integrations(): @app.delete('/api/v1/integrations/{integration_id}') -def delete_integration(integration_id: int): +def delete_integration(integration_id: int, request=None): db = models.SessionLocal() try: row = db.query(models.Integration).filter_by(id=integration_id).first() if not row: raise HTTPException(status_code=404, detail='integration not found') - db.delete(row) + # require owner or admin + from .rbac import require_owner_or_admin + require_owner_or_admin(row.user_id)(request) + db.delete(row) db.commit() return {'ok': True} finally: @@ -223,7 +238,10 @@ def sync_integration_to_habits(integration_id: int, payload: dict = Body({})): if not integration: raise HTTPException(status_code=404, detail='integration not found') - # Fetch events via existing events endpoint logic + # require owner or admin + from .rbac import require_owner_or_admin + require_owner_or_admin(integration.user_id)(None) + # Fetch events via existing events endpoint logic # Reuse token refresh + decrypt logic from oauth module token_row = db.query(models.OAuthToken).filter_by(integration_id=integration_id).order_by(models.OAuthToken.id.desc()).first() if not token_row: diff --git a/modern/backend/models.py b/modern/backend/models.py index b414cea..6464abc 100644 --- a/modern/backend/models.py +++ b/modern/backend/models.py @@ -15,6 +15,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, nullable=False, index=True) password_hash = Column(String) + role = Column(String, default='user') display_name = Column(String) created_at = Column(DateTime, server_default=func.current_timestamp()) updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp()) diff --git a/modern/backend/rbac.py b/modern/backend/rbac.py new file mode 100644 index 0000000..9aa98bf --- /dev/null +++ b/modern/backend/rbac.py @@ -0,0 +1,23 @@ +from fastapi import HTTPException +from .auth import get_current_user +from . import models + + +def require_role(min_role: str): + # Simple role hierarchy + hierarchy = {'user': 1, 'moderator': 2, 'admin': 3} + def _inner(request=None): + user = get_current_user(request) + if hierarchy.get(user.role, 0) < hierarchy.get(min_role, 0): + raise HTTPException(status_code=403, detail='insufficient role') + return user + return _inner + + +def require_owner_or_admin(resource_user_id: int): + def _inner(request=None): + user = get_current_user(request) + if user.id == resource_user_id or user.role == 'admin': + return user + raise HTTPException(status_code=403, detail='must be owner or admin') + return _inner diff --git a/modern/backend/requirements_full.txt b/modern/backend/requirements_full.txt index 4ae34d5..f1a86f4 100644 --- a/modern/backend/requirements_full.txt +++ b/modern/backend/requirements_full.txt @@ -6,3 +6,6 @@ python-dotenv requests cryptography boto3 +pytest +httpx +requests diff --git a/modern/frontend/index.html b/modern/frontend/index.html index 2041521..d91ff2b 100644 --- a/modern/frontend/index.html +++ b/modern/frontend/index.html @@ -1,12 +1,15 @@ -
+ +