feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE) Gateway - InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF / CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck() - HTTP 402 + JSON detail für globale API-Fehlerbehandlung - AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify - Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload - datamodelBilling: notifyEmails-Doku für Pool-Alerts frontend_nyla - useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En und Hinweis auf Billing-Pfad bei TOP_UP_SELF
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Security service for token management operations.
|
|
Core service - not requested by features directly.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional, Callable, Any
|
|
|
|
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
|
|
from modules.auth import TokenManager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SecurityService:
|
|
"""Security service providing token management operations."""
|
|
|
|
def __init__(self, context: Any, get_service: Callable[[str], Any]):
|
|
"""Initialize with service center context and resolver."""
|
|
self._context = context
|
|
self._get_service = get_service
|
|
self._tokenManager = TokenManager()
|
|
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
|
|
self._interfaceDbApp = getAppInterface(
|
|
context.user,
|
|
mandateId=context.mandate_id,
|
|
)
|
|
|
|
def getFreshToken(self, connectionId: str, secondsBeforeExpiry: int = 30 * 60) -> Optional[Token]:
|
|
"""Get a fresh token for a connection, refreshing when expiring soon."""
|
|
try:
|
|
token = self._interfaceDbApp.getConnectionToken(connectionId)
|
|
if not token:
|
|
return None
|
|
_tp = (
|
|
token.tokenPurpose.value
|
|
if isinstance(token.tokenPurpose, TokenPurpose)
|
|
else token.tokenPurpose
|
|
)
|
|
if _tp != TokenPurpose.DATA_CONNECTION.value:
|
|
logger.warning(
|
|
f"getFreshToken: connection {connectionId} tokenPurpose is {_tp}, expected dataConnection"
|
|
)
|
|
return None
|
|
return self._tokenManager.ensureFreshToken(
|
|
token,
|
|
secondsBeforeExpiry=secondsBeforeExpiry,
|
|
saveCallback=lambda t: self._interfaceDbApp.saveConnectionToken(t)
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"getFreshToken: Error fetching or refreshing token for connection {connectionId}: {e}")
|
|
return None
|
|
|
|
def refreshToken(self, oldToken: Token) -> Optional[Token]:
|
|
"""Refresh an expired token using the appropriate OAuth service."""
|
|
try:
|
|
return self._tokenManager.refreshToken(oldToken)
|
|
except Exception as e:
|
|
logger.error(f"refreshToken: Error refreshing token: {e}")
|
|
return None
|
|
|
|
def ensureFreshToken(self, token: Token, *, secondsBeforeExpiry: int = 30 * 60,
|
|
saveCallback: Optional[Callable[[Token], None]] = None) -> Optional[Token]:
|
|
"""Ensure a token is fresh; refresh if expiring within threshold."""
|
|
try:
|
|
return self._tokenManager.ensureFreshToken(
|
|
token,
|
|
secondsBeforeExpiry=secondsBeforeExpiry,
|
|
saveCallback=saveCallback
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"ensureFreshToken: Error ensuring fresh token: {e}")
|
|
return None
|
|
|
|
def refreshMicrosoftToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
|
|
"""Refresh Microsoft OAuth token using refresh token."""
|
|
try:
|
|
return self._tokenManager.refreshMicrosoftToken(refreshToken, userId, oldToken)
|
|
except Exception as e:
|
|
logger.error(f"refreshMicrosoftToken: Error refreshing Microsoft token: {e}")
|
|
return None
|
|
|
|
def refreshGoogleToken(self, refreshToken: str, userId: str, oldToken: Token) -> Optional[Token]:
|
|
"""Refresh Google OAuth token using refresh token."""
|
|
try:
|
|
return self._tokenManager.refreshGoogleToken(refreshToken, userId, oldToken)
|
|
except Exception as e:
|
|
logger.error(f"refreshGoogleToken: Error refreshing Google token: {e}")
|
|
return None
|