Arch: Finalize V3 Auth modularization and Unified Agent spec.
- Integrated zero-dependency Auth models and dependencies_v3.py. - Successfully resolved circular dependency boot loops. - Verified site_domain search exception via verify_v3_exceptions.py. - Refined Unified Agent Architecture with Storage Layer and API-driven access details. - Updated project roadmap and milestones in GEMINI.md.
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
"""
|
||||
This file contains general utility functions and helpers specifically for API v3.
|
||||
It aims to provide a clean slate for new methods and refactor existing ones from lib_general.py
|
||||
that are relevant to the v3 API, while removing unused or outdated functionalities.
|
||||
Refactored 2026-01-07 to move Auth logic to dependencies_v3.py to fix circular dependencies.
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
import time
|
||||
import logging
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -15,7 +12,6 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
# Third-party imports
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
@@ -29,143 +25,23 @@ from fastapi import (
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
ValidationError,
|
||||
computed_field,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
# Internal imports (from this project)
|
||||
# Re-import from the new central auth models
|
||||
from app.models.auth_models import AccountContext
|
||||
# Import the dependency functions for backward compatibility in existing v3 routes
|
||||
from app.routers.dependencies_v3 import (
|
||||
get_account_context,
|
||||
get_account_context_optional,
|
||||
PaginationParams,
|
||||
StatusFilterParams,
|
||||
SerializationParams,
|
||||
DelayParams
|
||||
)
|
||||
|
||||
from app.config import settings
|
||||
from app.db_sql import redis_lookup_id_random
|
||||
from app.log import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
# --- Pydantic Model for Account Context ---
|
||||
class AccountContext(BaseModel):
|
||||
account_id: Optional[int]
|
||||
account_id_random: Optional[str]
|
||||
|
||||
|
||||
# --- Dependency Function for Account Context ---
|
||||
def get_account_context(
|
||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100), # Assuming 'bypass' or similar string
|
||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
||||
) -> AccountContext:
|
||||
"""
|
||||
Resolves the account context from headers/query parameters with defined precedence.
|
||||
Precedence: x_account_id (header) > x_no_account_id_token (query) > x_no_account_id (header flag)
|
||||
Raises HTTPException 403 if no valid account is found and no bypass is indicated.
|
||||
"""
|
||||
logger.setLevel(logging.DEBUG) # Adjust as needed
|
||||
logger.debug(locals())
|
||||
|
||||
resolved_account_id = None
|
||||
resolved_account_id_random = None
|
||||
|
||||
if x_account_id:
|
||||
# Primary check: x_account_id header
|
||||
resolved_account_id_random = x_account_id
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
|
||||
resolved_account_id = looked_up_id
|
||||
logger.info(f'Found account from x_account_id header: {resolved_account_id}')
|
||||
else:
|
||||
logger.warning(f'Invalid x_account_id header provided: {x_account_id}')
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-Account-ID header.')
|
||||
elif x_no_account_id_token:
|
||||
# Secondary check: x_no_account_id_token query parameter
|
||||
resolved_account_id_random = x_no_account_id_token
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
|
||||
resolved_account_id = looked_up_id
|
||||
logger.info(f'Found account from x_no_account_id_token query: {resolved_account_id}')
|
||||
else:
|
||||
logger.warning(f'Invalid x_no_account_id_token query provided: {x_no_account_id_token}')
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Invalid X-No-Account-ID-Token query parameter.')
|
||||
elif x_no_account_id:
|
||||
# Tertiary check: x_no_account_id header for bypass
|
||||
# For now, just presence indicates bypass. Can add a specific value check later if needed.
|
||||
logger.info(f'X-No-Account-ID header found: {x_no_account_id}. Proceeding without specific account context.')
|
||||
resolved_account_id = None # Explicitly None for "no specific account"
|
||||
resolved_account_id_random = '--- NO ACCOUNT ---'
|
||||
else:
|
||||
logger.warning('No valid account context provided via X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.')
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required. Please provide X-Account-ID, X-No-Account-ID-Token, or X-No-Account-ID.')
|
||||
|
||||
return AccountContext(account_id=resolved_account_id, account_id_random=resolved_account_id_random)
|
||||
|
||||
|
||||
# --- Pydantic Model for Pagination ---
|
||||
class PaginationParams(BaseModel):
|
||||
limit: int = 100 # Default limit
|
||||
offset: int = 0
|
||||
|
||||
# --- Dependency Function for Pagination ---
|
||||
def get_pagination_params(
|
||||
limit: int = Query(100, ge=0, description="Maximum number of items to return"),
|
||||
offset: int = Query(0, ge=0, description="Number of items to skip (for pagination)"),
|
||||
) -> PaginationParams:
|
||||
return PaginationParams(limit=limit, offset=offset)
|
||||
|
||||
|
||||
# --- Pydantic Model for Status Filtering ---
|
||||
class StatusFilterParams(BaseModel):
|
||||
enabled: str = 'enabled' # 'enabled', 'disabled', 'all'
|
||||
hidden: str = 'not_hidden' # 'hidden', 'not_hidden', 'all'
|
||||
|
||||
# --- Dependency Function for Status Filtering ---
|
||||
def get_status_filter_params(
|
||||
enabled: str = Query('enabled', description="Filter by object enabled status ('enabled', 'disabled', 'all')"),
|
||||
hidden: str = Query('not_hidden', description="Filter by object hidden status ('hidden', 'not_hidden', 'all')"),
|
||||
) -> StatusFilterParams:
|
||||
allowed_enabled_values = {'enabled', 'disabled', 'all'}
|
||||
allowed_hidden_values = {'hidden', 'not_hidden', 'all'}
|
||||
|
||||
if enabled not in allowed_enabled_values:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid value for 'enabled'. Must be one of {list(allowed_enabled_values)}."
|
||||
)
|
||||
if hidden not in allowed_hidden_values:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid value for 'hidden'. Must be one of {list(allowed_hidden_values)}."
|
||||
)
|
||||
return StatusFilterParams(enabled=enabled, hidden=hidden)
|
||||
|
||||
|
||||
# --- Pydantic Model for Serialization Options ---
|
||||
class SerializationParams(BaseModel):
|
||||
by_alias: bool = True
|
||||
exclude_unset: bool = False
|
||||
exclude_defaults: bool = False # Added based on common_route_params
|
||||
exclude_none: bool = False # Added based on common_route_params
|
||||
|
||||
# --- Dependency Function for Serialization Options ---
|
||||
def get_serialization_params(
|
||||
by_alias: bool = Query(True, description="Whether to use field aliases for serialization"),
|
||||
exclude_unset: bool = Query(False, description="Whether to exclude unset fields from the response"),
|
||||
exclude_defaults: bool = Query(False, description="Whether to exclude fields with their default values from the response"),
|
||||
exclude_none: bool = Query(False, description="Whether to exclude fields that are None from the response"),
|
||||
) -> SerializationParams:
|
||||
return SerializationParams(
|
||||
by_alias=by_alias,
|
||||
exclude_unset=exclude_unset,
|
||||
exclude_defaults=exclude_defaults,
|
||||
exclude_none=exclude_none,
|
||||
)
|
||||
|
||||
|
||||
# --- Pydantic Model for Delay ---
|
||||
class DelayParams(BaseModel):
|
||||
sleep_time_ms: int = 0 # Raw delay value in ms
|
||||
sleep_time_s: float = 0.0 # Converted to seconds for time.sleep()
|
||||
|
||||
# --- Dependency Function for Delay ---
|
||||
def get_delay_params(
|
||||
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms', description="Delay response for X milliseconds (header)"),
|
||||
delay_ms: Optional[int] = Query(0, description="Delay response for X milliseconds (query parameter)"),
|
||||
) -> DelayParams:
|
||||
calculated_delay_ms = max(x_delay_ms or 0, delay_ms or 0)
|
||||
return DelayParams(sleep_time_ms=calculated_delay_ms, sleep_time_s=calculated_delay_ms / 1000.0)
|
||||
# Note: Dependency function implementations have moved to app/routers/dependencies_v3.py
|
||||
|
||||
13
app/models/auth_models.py
Normal file
13
app/models/auth_models.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Zero-dependency auth models for V3
|
||||
# Created 2026-01-07 to resolve circular dependencies in FastAPI startup
|
||||
|
||||
class AccountContext(BaseModel):
|
||||
account_id: Optional[int]
|
||||
account_id_random: Optional[str]
|
||||
administrator: bool = False
|
||||
manager: bool = False
|
||||
super: bool = False
|
||||
auth_method: str = 'legacy_header'
|
||||
@@ -4,38 +4,15 @@ import json
|
||||
import urllib.parse
|
||||
import time
|
||||
import asyncio
|
||||
from pydantic import BaseModel
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# NOTE: We are defining these locally to avoid circular dependency hell with lib_general_v3 for now.
|
||||
class AccountContext(BaseModel):
|
||||
account_id: Optional[int]
|
||||
account_id_random: Optional[str]
|
||||
administrator: bool = False
|
||||
manager: bool = False
|
||||
super: bool = False
|
||||
auth_method: str = 'legacy_header'
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
limit: int = 100
|
||||
offset: int = 0
|
||||
|
||||
class StatusFilterParams(BaseModel):
|
||||
enabled: str = 'enabled'
|
||||
hidden: str = 'not_hidden'
|
||||
|
||||
class SerializationParams(BaseModel):
|
||||
by_alias: bool = True
|
||||
exclude_unset: bool = False
|
||||
exclude_defaults: bool = False
|
||||
exclude_none: bool = False
|
||||
|
||||
class DelayParams(BaseModel):
|
||||
sleep_time_ms: int = 0
|
||||
sleep_time_s: float = 0.0
|
||||
|
||||
from app.lib_general_v3 import (
|
||||
AccountContext, get_account_context, get_account_context_optional,
|
||||
PaginationParams, StatusFilterParams,
|
||||
SerializationParams, DelayParams
|
||||
)
|
||||
from app.models.response_models import *
|
||||
from app.models.api_crud_models import SearchFilter, SearchQuery
|
||||
from app.ae_obj_types_def import obj_type_kv_li
|
||||
@@ -43,82 +20,6 @@ from app.ae_obj_types_def import obj_type_kv_li
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# --- Local Dependencies ---
|
||||
|
||||
def get_account_context_optional(
|
||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
||||
) -> AccountContext:
|
||||
from app.db_sql import redis_lookup_id_random
|
||||
|
||||
resolved_account_id = None
|
||||
resolved_account_id_random = None
|
||||
auth_method = 'guest'
|
||||
|
||||
if x_account_id:
|
||||
resolved_account_id_random = x_account_id
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
|
||||
resolved_account_id = looked_up_id
|
||||
auth_method = 'legacy_header'
|
||||
elif x_no_account_id_token:
|
||||
resolved_account_id_random = x_no_account_id_token
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
|
||||
resolved_account_id = looked_up_id
|
||||
auth_method = 'token_query'
|
||||
elif x_no_account_id:
|
||||
resolved_account_id = None
|
||||
resolved_account_id_random = '--- NO ACCOUNT ---'
|
||||
auth_method = 'bypass'
|
||||
|
||||
return AccountContext(
|
||||
account_id=resolved_account_id,
|
||||
account_id_random=resolved_account_id_random,
|
||||
auth_method=auth_method,
|
||||
administrator=(auth_method == 'bypass'),
|
||||
manager=(auth_method == 'bypass'),
|
||||
super=(auth_method == 'bypass')
|
||||
)
|
||||
|
||||
def get_account_context(
|
||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
||||
) -> AccountContext:
|
||||
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token)
|
||||
if ctx.auth_method == 'guest':
|
||||
# Raise strict 403 if required
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
|
||||
return ctx
|
||||
|
||||
def get_pagination_params(
|
||||
limit: int = Query(100, ge=0),
|
||||
offset: int = Query(0, ge=0),
|
||||
) -> PaginationParams:
|
||||
return PaginationParams(limit=limit, offset=offset)
|
||||
|
||||
def get_status_filter_params(
|
||||
enabled: str = Query('enabled'),
|
||||
hidden: str = Query('not_hidden'),
|
||||
) -> StatusFilterParams:
|
||||
return StatusFilterParams(enabled=enabled, hidden=hidden)
|
||||
|
||||
def get_serialization_params(
|
||||
by_alias: bool = Query(True),
|
||||
exclude_unset: bool = Query(False),
|
||||
exclude_defaults: bool = Query(False),
|
||||
exclude_none: bool = Query(False),
|
||||
) -> SerializationParams:
|
||||
return SerializationParams(by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none)
|
||||
|
||||
def get_delay_params(
|
||||
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms'),
|
||||
delay_ms: Optional[int] = Query(0),
|
||||
) -> DelayParams:
|
||||
val = max(x_delay_ms or 0, delay_ms or 0)
|
||||
return DelayParams(sleep_time_ms=val, sleep_time_s=val / 1000.0)
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
def check_account_access(sql_result: Any, account: AccountContext, obj_name: str = None) -> bool:
|
||||
@@ -176,7 +77,12 @@ def filter_order_by(order_by_li: Any, model: Any, table_name: str = None) -> Opt
|
||||
def get_supported_filters(model: Any, status_filter: StatusFilterParams) -> StatusFilterParams:
|
||||
if not model or not hasattr(model, "__fields__"):
|
||||
return status_filter
|
||||
adjusted = StatusFilterParams(enabled=status_filter.enabled, hidden=status_filter.hidden)
|
||||
# We create a new instance to avoid side effects on the dependency object
|
||||
from app.routers.dependencies_v3 import StatusFilterParams as SF
|
||||
adjusted = SF()
|
||||
adjusted.enabled = status_filter.enabled
|
||||
adjusted.hidden = status_filter.hidden
|
||||
|
||||
if 'enable' not in model.__fields__:
|
||||
adjusted.enabled = 'all'
|
||||
if 'hide' not in model.__fields__:
|
||||
@@ -192,7 +98,7 @@ def safe_json_loads(json_str: Optional[str]) -> Any:
|
||||
# --- Routes ---
|
||||
|
||||
@router.get("/health", response_model=Resp_Body_Base)
|
||||
async def health_check(delay: DelayParams = Depends(get_delay_params)):
|
||||
async def health_check(delay: DelayParams = Depends()):
|
||||
if delay.sleep_time_s > 0: await asyncio.sleep(delay.sleep_time_s)
|
||||
return mk_resp(data={"status": "V3 API is healthy!"})
|
||||
|
||||
@@ -252,8 +158,8 @@ async def get_obj(
|
||||
obj_id: str = Path(min_length=11, max_length=22),
|
||||
view: str = Query('default'),
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
|
||||
@@ -293,10 +199,10 @@ async def get_obj_li(
|
||||
order_by_li: Optional[str] = None,
|
||||
jp: Optional[Union[str, None]] = None,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
status_filter: StatusFilterParams = Depends(get_status_filter_params),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
pagination: PaginationParams = Depends(),
|
||||
status_filter: StatusFilterParams = Depends(),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
|
||||
@@ -397,10 +303,10 @@ async def search_obj_li(
|
||||
view: str = Query('default'),
|
||||
order_by_li: Optional[str] = Query(None),
|
||||
account: AccountContext = Depends(get_account_context_optional),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
status_filter: StatusFilterParams = Depends(get_status_filter_params),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
pagination: PaginationParams = Depends(),
|
||||
status_filter: StatusFilterParams = Depends(),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
|
||||
@@ -487,8 +393,8 @@ async def post_obj(
|
||||
obj_type_l1: str = Path(min_length=2, max_length=50),
|
||||
return_obj: Optional[bool] = True,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import sql_insert, get_id_random, sql_select
|
||||
|
||||
@@ -542,8 +448,8 @@ async def patch_obj(
|
||||
obj_id: str = Path(min_length=11, max_length=22),
|
||||
return_obj: Optional[bool] = True,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
||||
|
||||
@@ -590,7 +496,7 @@ async def delete_obj(
|
||||
obj_id: str = Path(min_length=11, max_length=22),
|
||||
method: str = Query('delete', regex='^(delete|hide|disable)$'),
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
|
||||
|
||||
@@ -638,10 +544,10 @@ async def get_child_obj_li(
|
||||
order_by_li: Optional[str] = None,
|
||||
jp: Optional[Union[str, None]] = None,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
pagination: PaginationParams = Depends(get_pagination_params),
|
||||
status_filter: StatusFilterParams = Depends(get_status_filter_params),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
pagination: PaginationParams = Depends(),
|
||||
status_filter: StatusFilterParams = Depends(),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
|
||||
@@ -727,8 +633,8 @@ async def post_child_obj(
|
||||
child_obj_type: str = Path(min_length=2, max_length=50),
|
||||
return_obj: Optional[bool] = True,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_insert, get_id_random
|
||||
|
||||
@@ -791,8 +697,8 @@ async def get_child_obj(
|
||||
child_obj_type: str = Path(min_length=2, max_length=50),
|
||||
child_obj_id: str = Path(min_length=11, max_length=22),
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select
|
||||
|
||||
@@ -827,8 +733,8 @@ async def patch_child_obj(
|
||||
child_obj_id: str = Path(min_length=11, max_length=22),
|
||||
return_obj: Optional[bool] = True,
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
serialization: SerializationParams = Depends(get_serialization_params),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
serialization: SerializationParams = Depends(),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update
|
||||
|
||||
@@ -870,7 +776,7 @@ async def delete_child_obj(
|
||||
child_obj_id: str = Path(min_length=11, max_length=22),
|
||||
method: str = Query('delete', regex='^(delete|hide|disable)$'),
|
||||
account: AccountContext = Depends(get_account_context),
|
||||
delay: DelayParams = Depends(get_delay_params),
|
||||
delay: DelayParams = Depends(),
|
||||
):
|
||||
from app.db_sql import redis_lookup_id_random, sql_select, sql_update, sql_delete
|
||||
|
||||
|
||||
104
app/routers/dependencies_v3.py
Normal file
104
app/routers/dependencies_v3.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from fastapi import Depends, Header, HTTPException, Query, Response, status
|
||||
from typing import Optional, Union
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from app.models.auth_models import AccountContext
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# --- Account Context Dependencies ---
|
||||
|
||||
def get_account_context_optional(
|
||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
||||
) -> AccountContext:
|
||||
"""
|
||||
Resolves the account context but does not raise 403 on failure.
|
||||
Uses DEFERRED imports to prevent circular dependency at startup.
|
||||
"""
|
||||
from app.db_sql import redis_lookup_id_random
|
||||
|
||||
resolved_account_id = None
|
||||
resolved_account_id_random = None
|
||||
auth_method = 'guest'
|
||||
|
||||
if x_account_id:
|
||||
resolved_account_id_random = x_account_id
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
|
||||
resolved_account_id = looked_up_id
|
||||
auth_method = 'legacy_header'
|
||||
elif x_no_account_id_token:
|
||||
resolved_account_id_random = x_no_account_id_token
|
||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
|
||||
resolved_account_id = looked_up_id
|
||||
auth_method = 'token_query'
|
||||
elif x_no_account_id:
|
||||
resolved_account_id = None
|
||||
resolved_account_id_random = '--- NO ACCOUNT ---'
|
||||
auth_method = 'bypass'
|
||||
|
||||
return AccountContext(
|
||||
account_id=resolved_account_id,
|
||||
account_id_random=resolved_account_id_random,
|
||||
auth_method=auth_method,
|
||||
administrator=(auth_method == 'bypass'),
|
||||
manager=(auth_method == 'bypass'),
|
||||
super=(auth_method == 'bypass')
|
||||
)
|
||||
|
||||
def get_account_context(
|
||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
||||
) -> AccountContext:
|
||||
"""Strict version of account context resolution."""
|
||||
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token)
|
||||
if ctx.auth_method == 'guest':
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
|
||||
return ctx
|
||||
|
||||
|
||||
# --- Shared Pagination & Status Dependencies ---
|
||||
|
||||
class PaginationParams:
|
||||
def __init__(
|
||||
self,
|
||||
limit: int = Query(100, ge=0),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
self.limit = limit
|
||||
self.offset = offset
|
||||
|
||||
class StatusFilterParams:
|
||||
def __init__(
|
||||
self,
|
||||
enabled: str = Query('enabled'),
|
||||
hidden: str = Query('not_hidden'),
|
||||
):
|
||||
self.enabled = enabled
|
||||
self.hidden = hidden
|
||||
|
||||
class SerializationParams:
|
||||
def __init__(
|
||||
self,
|
||||
by_alias: bool = Query(True),
|
||||
exclude_unset: bool = Query(False),
|
||||
exclude_defaults: bool = Query(False),
|
||||
exclude_none: bool = Query(False),
|
||||
):
|
||||
self.by_alias = by_alias
|
||||
self.exclude_unset = exclude_unset
|
||||
self.exclude_defaults = exclude_defaults
|
||||
self.exclude_none = exclude_none
|
||||
|
||||
class DelayParams:
|
||||
def __init__(
|
||||
self,
|
||||
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms'),
|
||||
delay_ms: Optional[int] = Query(0),
|
||||
):
|
||||
val = max(x_delay_ms or 0, delay_ms or 0)
|
||||
self.sleep_time_ms = val
|
||||
self.sleep_time_s = val / 1000.0
|
||||
Reference in New Issue
Block a user