Skip to content

presentation.routers.api.v1.providers

src.presentation.routers.api.v1.providers

Providers resource handlers.

Handler functions for provider connection endpoints. Routes are registered via ROUTE_REGISTRY in routes/registry.py.

Handlers

list_providers - List all connections for user get_provider_connection - Get connection details initiate_connection - Initiate OAuth connection oauth_callback - Handle OAuth callback update_provider - Update connection (alias) disconnect_provider - Disconnect provider refresh_provider_tokens - Refresh provider tokens

Reference
  • docs/architecture/api-design-patterns.md
  • docs/architecture/provider-oauth-architecture.md
  • docs/architecture/error-handling-architecture.md

Classes

Functions

list_providers async

list_providers(
    request: Request,
    current_user: AuthenticatedUser,
    active_only: Annotated[
        bool,
        Query(description="Only return active connections"),
    ] = False,
    handler: ListProviderConnectionsHandler = Depends(
        handler_factory(ListProviderConnectionsHandler)
    ),
) -> ProviderConnectionListResponse | JSONResponse

List all provider connections for user.

GET /api/v1/providers → 200 OK

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
active_only Annotated[bool, Query(description='Only return active connections')]

Filter to only active connections.

False
handler ListProviderConnectionsHandler

List connections handler (injected).

Depends(handler_factory(ListProviderConnectionsHandler))

Returns:

Type Description
ProviderConnectionListResponse | JSONResponse

ProviderConnectionListResponse with list of connections.

ProviderConnectionListResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def list_providers(
    request: Request,
    current_user: AuthenticatedUser,
    active_only: Annotated[
        bool,
        Query(description="Only return active connections"),
    ] = False,
    handler: ListProviderConnectionsHandler = Depends(
        handler_factory(ListProviderConnectionsHandler)
    ),
) -> ProviderConnectionListResponse | JSONResponse:
    """List all provider connections for user.

    GET /api/v1/providers → 200 OK

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        active_only: Filter to only active connections.
        handler: List connections handler (injected).

    Returns:
        ProviderConnectionListResponse with list of connections.
        JSONResponse with RFC 9457 error on failure.
    """
    query = ListProviderConnections(
        user_id=current_user.user_id,
        active_only=active_only,
    )
    result = await handler.handle(query)

    if isinstance(result, Failure):
        app_error = _map_provider_error(result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=get_trace_id() or "",
        )

    return ProviderConnectionListResponse.from_dto(result.value)

get_provider_connection async

get_provider_connection(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[
        UUID, Path(description="Connection UUID")
    ],
    handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse

Get a specific provider connection.

GET /api/v1/providers/{id} → 200 OK

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
connection_id Annotated[UUID, Path(description='Connection UUID')]

Connection UUID.

required
handler GetProviderConnectionHandler

Get connection handler (injected).

Depends(handler_factory(GetProviderConnectionHandler))

Returns:

Type Description
ProviderConnectionResponse | JSONResponse

ProviderConnectionResponse with connection details.

ProviderConnectionResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def get_provider_connection(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[UUID, Path(description="Connection UUID")],
    handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse:
    """Get a specific provider connection.

    GET /api/v1/providers/{id} → 200 OK

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        connection_id: Connection UUID.
        handler: Get connection handler (injected).

    Returns:
        ProviderConnectionResponse with connection details.
        JSONResponse with RFC 9457 error on failure.
    """
    query = GetProviderConnection(
        connection_id=connection_id,
        user_id=current_user.user_id,
    )
    result = await handler.handle(query)

    if isinstance(result, Failure):
        app_error = _map_provider_error(result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=get_trace_id() or "",
        )

    return ProviderConnectionResponse.from_dto(result.value)

initiate_connection async

initiate_connection(
    request: Request,
    current_user: AuthenticatedUser,
    data: ConnectProviderRequest,
    cache: CacheProtocol = Depends(get_cache),
) -> AuthorizationUrlResponse | JSONResponse

Initiate OAuth connection to a provider.

POST /api/v1/providers → 201 Created

Generates OAuth state, stores it in cache, and returns authorization URL. User should be redirected to this URL to complete OAuth consent.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
data ConnectProviderRequest

Connection request with provider_slug and optional alias.

required
cache CacheProtocol

Cache for storing OAuth state (injected).

Depends(get_cache)

Returns:

Type Description
AuthorizationUrlResponse | JSONResponse

AuthorizationUrlResponse with authorization URL and state.

AuthorizationUrlResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def initiate_connection(
    request: Request,
    current_user: AuthenticatedUser,
    data: ConnectProviderRequest,
    cache: CacheProtocol = Depends(get_cache),
) -> AuthorizationUrlResponse | JSONResponse:
    """Initiate OAuth connection to a provider.

    POST /api/v1/providers → 201 Created

    Generates OAuth state, stores it in cache, and returns authorization URL.
    User should be redirected to this URL to complete OAuth consent.

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        data: Connection request with provider_slug and optional alias.
        cache: Cache for storing OAuth state (injected).

    Returns:
        AuthorizationUrlResponse with authorization URL and state.
        JSONResponse with RFC 9457 error on failure.
    """
    # Validate provider is supported
    if data.provider_slug not in PROVIDER_REGISTRY:
        app_error = ApplicationError(
            code=ApplicationErrorCode.NOT_FOUND,
            message=f"Provider '{data.provider_slug}' not supported",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=get_trace_id() or "",
        )

    # Generate cryptographically secure state token
    state = secrets.token_urlsafe(32)

    # Store state in cache with user info for callback validation
    cache_key = f"{OAUTH_STATE_PREFIX}{state}"
    cache_value = f"{current_user.user_id}:{data.provider_slug}:{data.alias or ''}"
    await cache.set(cache_key, cache_value, ttl=OAUTH_STATE_TTL)

    # Build authorization URL
    auth_url = _build_schwab_auth_url(state, data.redirect_uri)

    return AuthorizationUrlResponse(
        authorization_url=auth_url,
        state=state,
        expires_in=OAUTH_STATE_TTL,
    )

oauth_callback async

oauth_callback(
    request: Request,
    code: Annotated[
        str,
        Query(
            description="Authorization code from provider"
        ),
    ],
    state: Annotated[
        str, Query(description="CSRF state token")
    ],
    cache: CacheProtocol = Depends(get_cache),
    encryption: EncryptionService = Depends(
        get_encryption_service
    ),
    connect_handler: ConnectProviderHandler = Depends(
        handler_factory(ConnectProviderHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse

Complete OAuth callback and create connection.

POST /api/v1/providers/callback?code=xxx&state=yyy → 201 Created

Validates state, exchanges code for tokens, encrypts credentials, and creates provider connection record.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
code Annotated[str, Query(description='Authorization code from provider')]

Authorization code from OAuth redirect.

required
state Annotated[str, Query(description='CSRF state token')]

State token from OAuth redirect.

required
cache CacheProtocol

Cache for retrieving stored state (injected).

Depends(get_cache)
encryption EncryptionService

Encryption service for credentials (injected).

Depends(get_encryption_service)
connect_handler ConnectProviderHandler

Connection handler (injected).

Depends(handler_factory(ConnectProviderHandler))

Returns:

Type Description
ProviderConnectionResponse | JSONResponse

ProviderConnectionResponse with new connection details.

ProviderConnectionResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def oauth_callback(
    request: Request,
    code: Annotated[str, Query(description="Authorization code from provider")],
    state: Annotated[str, Query(description="CSRF state token")],
    cache: CacheProtocol = Depends(get_cache),
    encryption: EncryptionService = Depends(get_encryption_service),
    connect_handler: ConnectProviderHandler = Depends(
        handler_factory(ConnectProviderHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse:
    """Complete OAuth callback and create connection.

    POST /api/v1/providers/callback?code=xxx&state=yyy → 201 Created

    Validates state, exchanges code for tokens, encrypts credentials,
    and creates provider connection record.

    Args:
        request: FastAPI request object.
        code: Authorization code from OAuth redirect.
        state: State token from OAuth redirect.
        cache: Cache for retrieving stored state (injected).
        encryption: Encryption service for credentials (injected).
        connect_handler: Connection handler (injected).

    Returns:
        ProviderConnectionResponse with new connection details.
        JSONResponse with RFC 9457 error on failure.
    """
    trace_id = get_trace_id() or ""

    # Step 1: Validate and retrieve state from cache
    cache_key = f"{OAUTH_STATE_PREFIX}{state}"
    cache_result = await cache.get(cache_key)

    # Cache returns Result[str | None, CacheError] - handle the result
    if isinstance(cache_result, Failure) or cache_result.value is None:
        app_error = ApplicationError(
            code=ApplicationErrorCode.COMMAND_VALIDATION_FAILED,
            message="Invalid or expired OAuth state",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    cached_value = cache_result.value

    # Delete state to prevent replay attacks
    await cache.delete(cache_key)

    # Parse cached state: "user_id:provider_slug:alias"
    parts = cached_value.split(":", 2)
    if len(parts) < 2:
        app_error = ApplicationError(
            code=ApplicationErrorCode.COMMAND_VALIDATION_FAILED,
            message="Corrupted OAuth state",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    user_id = UUID(parts[0])
    provider_slug = parts[1]
    alias = parts[2] if len(parts) > 2 and parts[2] else None

    # Step 2: Validate provider and exchange code for tokens
    if provider_slug not in PROVIDER_REGISTRY:
        app_error = ApplicationError(
            code=ApplicationErrorCode.NOT_FOUND,
            message=f"Provider '{provider_slug}' not supported",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    # Get provider and verify OAuth capability
    try:
        provider = get_provider(provider_slug)
    except ValueError as e:
        app_error = ApplicationError(
            code=ApplicationErrorCode.NOT_FOUND,
            message=str(e),
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    if not is_oauth_provider(provider):
        app_error = ApplicationError(
            code=ApplicationErrorCode.COMMAND_VALIDATION_FAILED,
            message=f"Provider '{provider_slug}' does not support OAuth",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    # Exchange code for tokens (type narrowed to OAuthProviderProtocol)
    token_result = await provider.exchange_code_for_tokens(code)

    if isinstance(token_result, Failure):
        # Provider errors map to BAD_GATEWAY (502)
        app_error = ApplicationError(
            code=ApplicationErrorCode.COMMAND_EXECUTION_FAILED,
            message=f"Provider authentication failed: {token_result.error.message}",
        )
        # Return 502 Bad Gateway for provider failures
        from src.presentation.routers.api.v1.errors import ProblemDetails

        problem = ProblemDetails(
            type=f"{settings.api_base_url}/errors/provider_error",
            title="Provider Error",
            status=status.HTTP_502_BAD_GATEWAY,
            detail=app_error.message,
            instance=str(request.url.path),
            errors=None,
            trace_id=trace_id,
        )
        return JSONResponse(
            status_code=status.HTTP_502_BAD_GATEWAY,
            content=problem.model_dump(exclude_none=True),
        )

    tokens: OAuthTokens = token_result.value

    # Step 3: Encrypt credentials as opaque blob
    # Domain layer stores credentials as encrypted bytes
    now = datetime.now(UTC)
    credentials_dict = {
        "access_token": tokens.access_token,
        "refresh_token": tokens.refresh_token,
        "token_type": tokens.token_type,
        "expires_in": tokens.expires_in,
        "scope": tokens.scope,
    }
    encrypt_result = encryption.encrypt(credentials_dict)

    if isinstance(encrypt_result, Failure):
        app_error = ApplicationError(
            code=ApplicationErrorCode.COMMAND_EXECUTION_FAILED,
            message="Failed to encrypt credentials",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    credentials = ProviderCredentials(
        encrypted_data=encrypt_result.value,
        credential_type=CredentialType.OAUTH2,
        expires_at=now + timedelta(seconds=tokens.expires_in),
    )

    # Step 4: Create connection via handler
    provider_id = PROVIDER_REGISTRY[provider_slug]
    command = ConnectProvider(
        user_id=user_id,
        provider_id=provider_id,
        provider_slug=provider_slug,
        credentials=credentials,
        alias=alias,
    )
    connect_result = await connect_handler.handle(command)

    if isinstance(connect_result, Failure):
        app_error = _map_provider_error(connect_result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    # Step 5: Fetch and return the created connection
    # For now, construct response directly from known data
    from src.application.queries.handlers.get_provider_handler import (
        ProviderConnectionResult,
    )
    from src.domain.enums.connection_status import ConnectionStatus

    dto = ProviderConnectionResult(
        id=connect_result.value,
        user_id=user_id,
        provider_id=provider_id,
        provider_slug=provider_slug,
        alias=alias,
        status=ConnectionStatus.ACTIVE,
        is_connected=True,
        needs_reauthentication=False,
        connected_at=now,
        last_sync_at=None,
        created_at=now,
        updated_at=now,
    )

    return ProviderConnectionResponse.from_dto(dto)

update_provider async

update_provider(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[
        UUID, Path(description="Connection UUID")
    ],
    data: UpdateProviderConnectionRequest,
    get_handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse

Update a provider connection.

PATCH /api/v1/providers/{id} → 200 OK

Currently only supports updating the alias. Future versions may support additional fields.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
connection_id Annotated[UUID, Path(description='Connection UUID')]

Connection UUID.

required
data UpdateProviderConnectionRequest

Update request with new alias.

required
get_handler GetProviderConnectionHandler

Get handler for returning updated connection.

Depends(handler_factory(GetProviderConnectionHandler))

Returns:

Type Description
ProviderConnectionResponse | JSONResponse

ProviderConnectionResponse with updated connection.

ProviderConnectionResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Note

Full update implementation requires repository update method. For Phase 5, this is a simplified implementation.

Source code in src/presentation/routers/api/v1/providers.py
async def update_provider(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[UUID, Path(description="Connection UUID")],
    data: UpdateProviderConnectionRequest,
    get_handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
) -> ProviderConnectionResponse | JSONResponse:
    """Update a provider connection.

    PATCH /api/v1/providers/{id} → 200 OK

    Currently only supports updating the alias. Future versions may
    support additional fields.

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        connection_id: Connection UUID.
        data: Update request with new alias.
        get_handler: Get handler for returning updated connection.

    Returns:
        ProviderConnectionResponse with updated connection.
        JSONResponse with RFC 9457 error on failure.

    Note:
        Full update implementation requires repository update method.
        For Phase 5, this is a simplified implementation.
    """
    # Verify ownership by attempting to get the connection
    query = GetProviderConnection(
        connection_id=connection_id,
        user_id=current_user.user_id,
    )
    result = await get_handler.handle(query)

    if isinstance(result, Failure):
        app_error = _map_provider_error(result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=get_trace_id() or "",
        )

    # TODO: Implement actual update when repository supports it
    # For now, return the connection with the alias change acknowledged
    # This is a known limitation documented for Phase 5

    return ProviderConnectionResponse.from_dto(result.value)

disconnect_provider async

disconnect_provider(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[
        UUID, Path(description="Connection UUID")
    ],
    handler: DisconnectProviderHandler = Depends(
        handler_factory(DisconnectProviderHandler)
    ),
) -> Response | JSONResponse

Disconnect a provider connection.

DELETE /api/v1/providers/{id} → 204 No Content

Transitions connection to DISCONNECTED status and clears credentials. The connection record is kept for audit purposes.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
connection_id Annotated[UUID, Path(description='Connection UUID')]

Connection UUID.

required
handler DisconnectProviderHandler

Disconnect handler (injected).

Depends(handler_factory(DisconnectProviderHandler))

Returns:

Type Description
Response | JSONResponse

Response (204 No Content) on success.

Response | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def disconnect_provider(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[UUID, Path(description="Connection UUID")],
    handler: DisconnectProviderHandler = Depends(
        handler_factory(DisconnectProviderHandler)
    ),
) -> Response | JSONResponse:
    """Disconnect a provider connection.

    DELETE /api/v1/providers/{id} → 204 No Content

    Transitions connection to DISCONNECTED status and clears credentials.
    The connection record is kept for audit purposes.

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        connection_id: Connection UUID.
        handler: Disconnect handler (injected).

    Returns:
        Response (204 No Content) on success.
        JSONResponse with RFC 9457 error on failure.
    """
    command = DisconnectProvider(
        user_id=current_user.user_id,
        connection_id=connection_id,
    )
    result = await handler.handle(command)

    if isinstance(result, Failure):
        app_error = _map_provider_error(result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=get_trace_id() or "",
        )

    return Response(status_code=status.HTTP_204_NO_CONTENT)

refresh_provider_tokens async

refresh_provider_tokens(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[
        UUID, Path(description="Connection UUID")
    ],
    data: RefreshProviderTokensRequest | None = None,
    get_handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
    refresh_handler: RefreshProviderTokensHandler = Depends(
        handler_factory(RefreshProviderTokensHandler)
    ),
    encryption: EncryptionService = Depends(
        get_encryption_service
    ),
) -> TokenRefreshResponse | JSONResponse

Refresh OAuth tokens for a provider connection.

POST /api/v1/providers/{id}/token-refreshes → 201 Created

Decrypts current refresh token, calls provider to refresh, encrypts new tokens, and updates connection credentials.

Parameters:

Name Type Description Default
request Request

FastAPI request object.

required
current_user AuthenticatedUser

Authenticated user (from JWT).

required
connection_id Annotated[UUID, Path(description='Connection UUID')]

Connection UUID.

required
data RefreshProviderTokensRequest | None

Optional refresh request with force flag.

None
get_handler GetProviderConnectionHandler

Get handler for fetching connection.

Depends(handler_factory(GetProviderConnectionHandler))
refresh_handler RefreshProviderTokensHandler

Token refresh handler (injected).

Depends(handler_factory(RefreshProviderTokensHandler))
encryption EncryptionService

Encryption service for credentials.

Depends(get_encryption_service)

Returns:

Type Description
TokenRefreshResponse | JSONResponse

TokenRefreshResponse with refresh result.

TokenRefreshResponse | JSONResponse

JSONResponse with RFC 9457 error on failure.

Source code in src/presentation/routers/api/v1/providers.py
async def refresh_provider_tokens(
    request: Request,
    current_user: AuthenticatedUser,
    connection_id: Annotated[UUID, Path(description="Connection UUID")],
    data: RefreshProviderTokensRequest | None = None,
    get_handler: GetProviderConnectionHandler = Depends(
        handler_factory(GetProviderConnectionHandler)
    ),
    refresh_handler: RefreshProviderTokensHandler = Depends(
        handler_factory(RefreshProviderTokensHandler)
    ),
    encryption: EncryptionService = Depends(get_encryption_service),
) -> TokenRefreshResponse | JSONResponse:
    """Refresh OAuth tokens for a provider connection.

    POST /api/v1/providers/{id}/token-refreshes → 201 Created

    Decrypts current refresh token, calls provider to refresh,
    encrypts new tokens, and updates connection credentials.

    Args:
        request: FastAPI request object.
        current_user: Authenticated user (from JWT).
        connection_id: Connection UUID.
        data: Optional refresh request with force flag.
        get_handler: Get handler for fetching connection.
        refresh_handler: Token refresh handler (injected).
        encryption: Encryption service for credentials.

    Returns:
        TokenRefreshResponse with refresh result.
        JSONResponse with RFC 9457 error on failure.
    """
    trace_id = get_trace_id() or ""

    # Step 1: Get connection and verify ownership
    query = GetProviderConnection(
        connection_id=connection_id,
        user_id=current_user.user_id,
    )
    get_result = await get_handler.handle(query)

    if isinstance(get_result, Failure):
        app_error = _map_provider_error(get_result.error)
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    connection_dto = get_result.value

    # Step 2: Verify connection is active
    if not connection_dto.is_connected:
        app_error = ApplicationError(
            code=ApplicationErrorCode.FORBIDDEN,
            message="Connection is not active",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    # Step 3: Validate provider slug is supported
    # Note: This is a simplified flow. In production, we'd need to
    # fetch the actual connection entity with credentials.
    if connection_dto.provider_slug not in PROVIDER_REGISTRY:
        app_error = ApplicationError(
            code=ApplicationErrorCode.NOT_FOUND,
            message=f"Provider '{connection_dto.provider_slug}' not supported",
        )
        return ErrorResponseBuilder.from_application_error(
            error=app_error,
            request=request,
            trace_id=trace_id,
        )

    # For Phase 5, we need to fetch credentials from repository
    # This demonstrates the endpoint structure; full implementation
    # requires additional repository method or credential storage

    # Placeholder response for Phase 5 MVP
    # Full implementation would:
    # 1. Fetch connection with credentials from repository
    # 2. Decrypt refresh token
    # 3. Call provider.refresh_access_token()
    # 4. Encrypt new tokens
    # 5. Call refresh_handler.handle()

    now = datetime.now(UTC)
    return TokenRefreshResponse(
        success=True,
        message="Token refresh scheduled",
        expires_at=now + timedelta(minutes=30),
    )