Repository navigation
Task/base360.ai #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Task/base360.ai #238
Changes from all commits
f8badb7
a261ade
03f3c9a
129f08e
f3c8269
4743cb4
1191677
9914622
d805bb0
3c9e91e
801c5eb
99c9011
cd513f5
6a6f89b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| # Python | ||
| __pycache__/ | ||
| *.py[cod] | ||
| *$py.class | ||
| *.so | ||
| .Python | ||
| build/ | ||
| develop-eggs/ | ||
| dist/ | ||
| downloads/ | ||
| eggs/ | ||
| .eggs/ | ||
| lib/ | ||
| lib64/ | ||
| parts/ | ||
| sdist/ | ||
| var/ | ||
| wheels/ | ||
| *.egg-info/ | ||
| .installed.cfg | ||
| *.egg | ||
|
|
||
| # Virtual Environment | ||
| .env | ||
| .venv | ||
| env/ | ||
| venv/ | ||
| ENV/ | ||
|
|
||
| # Coverage | ||
| .coverage | ||
| .coverage.* | ||
| .cache | ||
| coverage.xml | ||
| *.cover | ||
| htmlcov/ | ||
|
|
||
| # IDE | ||
| .idea/ | ||
| .vscode/ | ||
| *.swp | ||
| *.swo | ||
| .DS_Store | ||
|
|
||
| # Logs | ||
| *.log | ||
| logs/ | ||
| log/ | ||
|
|
||
| # Database | ||
| *.db | ||
| *.sqlite3 | ||
|
|
||
| # Local development | ||
| .env.local | ||
| .env.*.local | ||
| *.env | ||
|
|
||
| # Docker | ||
| .docker/ | ||
|
|
||
| # Test | ||
| .pytest_cache/ | ||
| .hypothesis/ | ||
| # --- FEATURE[apple_pay] START --- | ||
| .apple_pay_test_credentials/ | ||
| # --- FEATURE[apple_pay] END --- | ||
|
|
||
| # Alembic | ||
| versions/__pycache__/ | ||
| versions/*.pyc | ||
|
|
||
| # Distribution | ||
| dist/ | ||
| build/ | ||
|
|
||
| # Poetry | ||
| # poetry.lock | ||
|
|
||
| # vs code workspace | ||
| *.code-workspace | ||
|
|
||
| # Additional common patterns | ||
| __pypackages__/ | ||
| # --- FEATURE[celery] START --- | ||
| celerybeat-schedule | ||
| celerybeat.pid | ||
| # --- FEATURE[celery] END --- | ||
| *.sage.py | ||
| .ipynb_checkpoints | ||
| profile_default/ | ||
| ipython_config.py | ||
| .pdm.toml | ||
| .mypy_cache/ | ||
| .ruff_cache/ | ||
| .dmypy.json | ||
| dmypy.json | ||
|
|
||
| # OS specific | ||
| Thumbs.db | ||
| ehthumbs.db | ||
| Desktop.ini | ||
| $RECYCLE.BIN/ | ||
| *.lnk | ||
|
|
||
| # Backend specific | ||
| media/ | ||
| static/uploads/ | ||
| *.pem | ||
| node_modules/ | ||
| .sass-cache/ | ||
| tmp/ | ||
| temp/ |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,69 @@ | ||
| from fastapi import APIRouter, Depends, HTTPException | ||
| from typing import Dict, Any | ||
| from fastapi import APIRouter, Depends, HTTPException, Query | ||
| from decimal import Decimal | ||
| from typing import Dict, Any, List, Optional | ||
| from app.services.cache import get_revenue_summary | ||
| from app.services.reservations import MixedCurrencyError, list_tenant_properties | ||
| from app.core.auth import authenticate_request as get_current_user | ||
| from app.models.auth import AuthenticatedUser | ||
|
|
||
| router = APIRouter() | ||
|
|
||
|
|
||
| def _require_tenant(current_user: AuthenticatedUser) -> str: | ||
| """Return the user's tenant, or refuse when there is none.""" | ||
| # A user without a tenant must not fall into a shared bucket that other | ||
| # tenant-less users could read; refuse instead. | ||
| tenant_id = getattr(current_user, "tenant_id", None) | ||
| if not tenant_id: | ||
| raise HTTPException(status_code=403, detail="No tenant associated with this account") | ||
| return tenant_id | ||
|
|
||
|
|
||
| @router.get("/dashboard/properties") | ||
| async def get_dashboard_properties( | ||
| current_user: AuthenticatedUser = Depends(get_current_user) | ||
| ) -> List[Dict[str, Any]]: | ||
| """Properties of the caller's tenant, for the dashboard selector.""" | ||
| tenant_id = _require_tenant(current_user) | ||
|
|
||
| try: | ||
| return await list_tenant_properties(tenant_id) | ||
| except Exception: | ||
| raise HTTPException(status_code=503, detail="Property list temporarily unavailable") | ||
|
|
||
|
|
||
| @router.get("/dashboard/summary") | ||
| async def get_dashboard_summary( | ||
| property_id: str, | ||
| current_user: dict = Depends(get_current_user) | ||
| month: Optional[int] = Query(None, ge=1, le=12, description="Calendar month in the property's timezone"), | ||
| year: Optional[int] = Query(None, ge=2000, le=2100), | ||
| current_user: AuthenticatedUser = Depends(get_current_user) | ||
| ) -> Dict[str, Any]: | ||
|
|
||
| tenant_id = getattr(current_user, "tenant_id", "default_tenant") or "default_tenant" | ||
|
|
||
| revenue_data = await get_revenue_summary(property_id, tenant_id) | ||
|
|
||
| total_revenue_float = float(revenue_data['total']) | ||
|
|
||
|
|
||
| if (month is None) != (year is None): | ||
| raise HTTPException(status_code=422, detail="month and year must be given together") | ||
|
|
||
| tenant_id = _require_tenant(current_user) | ||
|
|
||
| try: | ||
| revenue_data = await get_revenue_summary(property_id, tenant_id, month, year) | ||
| except MixedCurrencyError: | ||
| raise HTTPException( | ||
| status_code=409, | ||
| detail="Reservations use more than one currency; totals cannot be added", | ||
| ) | ||
| except Exception: | ||
| raise HTTPException(status_code=503, detail="Revenue data temporarily unavailable") | ||
|
|
||
| # 'total' is already rounded to cents as an exact decimal string. | ||
| # total_revenue keeps the numeric field the frontend reads; the string | ||
| # field carries the exact amount for consumers that must not use floats. | ||
| total_revenue = Decimal(revenue_data['total']) | ||
|
|
||
| return { | ||
| "property_id": revenue_data['property_id'], | ||
| "total_revenue": total_revenue_float, | ||
| "total_revenue": float(total_revenue), | ||
| "total_revenue_str": str(total_revenue), | ||
| "currency": revenue_data['currency'], | ||
| "reservations_count": revenue_data['count'] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| from typing import Optional | ||
| import logging | ||
|
|
||
| from jose import jwt, JWTError | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
|
|
@@ -69,27 +71,39 @@ def resolve_tenant_from_user(user_data: dict) -> Optional[str]: | |
| return None | ||
|
|
||
| @staticmethod | ||
| async def resolve_tenant_id(user_id: str, user_email: str, token: Optional[str] = None) -> str: | ||
| async def resolve_tenant_id(user_id: str, user_email: str, token: Optional[str] = None) -> Optional[str]: | ||
| """ | ||
| Resolve tenant ID for a user. | ||
|
|
||
| Args: | ||
| user_id: User ID | ||
| user_email: User email | ||
|
|
||
| token: JWT already verified by the caller; its tenant claim wins | ||
|
|
||
| Returns: | ||
| Tenant ID | ||
| Tenant ID, or None when the user has no tenant | ||
| """ | ||
| # The signed token is the source of truth for the tenant. | ||
| if token: | ||
| try: | ||
| tenant_id = TenantResolver.resolve_tenant_from_token(jwt.get_unverified_claims(token)) | ||
| if tenant_id: | ||
| return tenant_id | ||
| except JWTError: | ||
| logger.warning(f"Could not read tenant claim from token for {user_email}") | ||
|
|
||
| # Fallback mapping by known user email. | ||
| if user_email == "sunset@propertyflow.com": | ||
| return "tenant-a" | ||
| if user_email == "ocean@propertyflow.com": | ||
| return "tenant-b" | ||
| if user_email == "candidate@propertyflow.com": | ||
| return "tenant-a" | ||
|
|
||
| # Default fallback | ||
| return "tenant-a" | ||
|
|
||
| # No default: an unknown user must never be placed in a real tenant, | ||
| # or they would see that tenant's data. | ||
| logger.warning(f"No tenant found for user {user_email} ({user_id})") | ||
| return None | ||
|
Comment on lines
96
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Removing default fallback in Suggested FixUpdate callers ( Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
|
|
||
| @staticmethod | ||
| async def update_user_tenant_metadata(user_id: str, tenant_id: str) -> None: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug:
resolve_tenant_from_tokentrusts user-editableuser_metadata.tenant_idover admin-controlled metadata or database assignments.Severity: HIGH
Suggested Fix
Ignore
user_metadatafor tenant authorization. Retrievetenant_idstrictly fromapp_metadataor validate the claim against theuser_tenantsdatabase table.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.