admin interface

This commit is contained in:
ValueOn AG 2025-09-22 07:44:39 +02:00
parent 168d66d167
commit ebfdd9ab03
14 changed files with 1526 additions and 341 deletions

14
app.py
View file

@ -274,6 +274,17 @@ app.add_middleware(
max_age=86400 # Increased caching for preflight requests
)
# CSRF protection middleware
from modules.security.csrf import CSRFMiddleware
from modules.security.tokenRefreshMiddleware import TokenRefreshMiddleware, ProactiveTokenRefreshMiddleware
app.add_middleware(CSRFMiddleware)
# Token refresh middleware (silent refresh for expired OAuth tokens)
app.add_middleware(TokenRefreshMiddleware, enabled=True)
# Proactive token refresh middleware (refresh tokens before they expire)
app.add_middleware(ProactiveTokenRefreshMiddleware, enabled=True, check_interval_minutes=5)
# Include all routers
from modules.routes.routeAdmin import router as generalRouter
app.include_router(generalRouter)
@ -290,6 +301,9 @@ app.include_router(userRouter)
from modules.routes.routeDataFiles import router as fileRouter
app.include_router(fileRouter)
from modules.routes.routeDataNeutralization import router as neutralizationRouter
app.include_router(neutralizationRouter)
from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(promptRouter)

View file

@ -10,25 +10,25 @@ APP_KEY_SYSVAR = CONFIG_KEY
DB_APP_HOST=gateway-int-server.postgres.database.azure.com
DB_APP_DATABASE=poweron_app
DB_APP_USER=heeshkdlby
DB_APP_PASSWORD_SECRET=VkAjgECESbEVQ$Tu
DB_APP_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8wTnBISUNVLWVobHJzX0xtS0pMcV9neXY3S05qc1F6RU9SRTdHM2F2VW1ldVlMYU9zRTU2OE9QTDBmcGRjN3ZUb1dobGZrUHZrR2EyWURtUXRYWk5MTExMVUJxY01yaFBTWFE4OTlHNHBsWHFSUnc9
DB_APP_PORT=5432
# PostgreSQL Storage (new)
DB_CHAT_HOST=gateway-int-server.postgres.database.azure.com
DB_CHAT_DATABASE=poweron_chat
DB_CHAT_USER=heeshkdlby
DB_CHAT_PASSWORD_SECRET=VkAjgECESbEVQ$Tu
DB_CHAT_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8wTnBIVmhCSEtCcDF6dXBCSkVzOTdaNUZVOUgtZ2JQQ3lMUjVKdUgxbnBkZHdPSFE5amNWTzhKNW4zcV9QSFdNakFVNXRVcDVlTnd4Tm51QjA2MTVVMVY1b3dBZHhQZXZLdUlsc3lBektKRjhIUXc9
DB_CHAT_PORT=5432
# PostgreSQL Storage (new)
DB_MANAGEMENT_HOST=gateway-int-server.postgres.database.azure.com
DB_MANAGEMENT_DATABASE=poweron_management
DB_MANAGEMENT_USER=heeshkdlby
DB_MANAGEMENT_PASSWORD_SECRET=VkAjgECESbEVQ$Tu
DB_MANAGEMENT_PASSWORD_SECRET = INT_ENC:Z0FBQUFBQm8wTnBISHA2OXVrWjhaQURZM3g4WGxiTmt3WW05WXBIRGVwNFNfdmphOGdUQ0ZCMUdFTlAzZlJTM2ZFaEhVWGRqNXBtREpTalItcDNxS1BJeEZKdWc0dWxHUm41QTBMZ3VqT3pHeFVmVUtJWE1YbTA9
DB_MANAGEMENT_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET=rotated_jwt_secret_2025_09_17_2c5f8e7a-1b3d-49c7-ae5d-9f0a2c3d4b5e
APP_JWT_KEY_SECRET = INT_ENC:Z0FBQUFBQm8wTnBIVXVUQnhWcjhvVFhtTDl5T1M1SXZZdjZDY0tIa0hmbnRuanUweUdoQ04xNzhod3VscG44V0xlNldzY2t1MVE5UjVjUTdSRUU1N3VBUGNVN0ozU0o1akNBX0x0X1FNOGE0TE9paTh0ZEVnZmNTbGFnSjBpNTBXMTZxemJwWmRTdkJOWms4VVRieGpSM3VtaFY3Zmw0NlJTbVVfbDdwYldVYUlfbGVFUGhsajVZPQ==
APP_TOKEN_EXPIRY=300
# CORS Configuration
@ -51,41 +51,29 @@ Service_GOOGLE_REDIRECT_URI = https://gateway-int.poweron-center.net/api/google/
# OpenAI configuration
Connector_AiOpenai_API_URL = https://api.openai.com/v1/chat/completions
Connector_AiOpenai_API_SECRET = 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
Connector_AiOpenai_API_SECRET = INT_ENC:Z0FBQUFBQm8wTnBIS0RqLW13RThlbTNNYUdLa3pXeFVYVm5Mc0czREtRczRBSlVjcVJJcVpKU19kRUZTU2pqMGZFR2pHZnZ4TGdMeFJqbHl5aTYwa2pzcTlNZklnMUNIZHZwdGFuWFhGZDlkemI2cnJuRURBZVBmM3Fxbm91c0ZQai1UMGJSM29kanIzMFB4Z2x6QWcycVk2SzRHQXc2YmZRPT0=
Connector_AiOpenai_MODEL_NAME = gpt-4o
Connector_AiOpenai_TEMPERATURE = 0.2
Connector_AiOpenai_MAX_TOKENS = 2000
# Anthropic configuration
Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = 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
Connector_AiAnthropic_API_SECRET = INT_ENC:Z0FBQUFBQm8wTnBIN0pPeHE3SzFWbTNySU1NRThmcURKWWNiZ3pQLTlwSXZmd0JkTUxXb2VGTVIyeUhZb2JKRzJsQ1AwTlZBWl9RYkRaQkVoR3dxQkdGYUFmd0xRdW1jUGxXdjJPbDlDVTVtT1c3aldRVVNoWmRLd09TZW5xU1JOVHp1ZE5Za0xBODR1TlhMQ1ZiaEZ4Nm00QnpPSks4RGVxYUhqaGdvMWVwMzBKSTdIUEVXSE1XM1ZNUjNBWDAzLWxwLXlib29OV0pOV21MTkFpb0ZDLU5seHMyTldxSFdIZz09
Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
Connector_AiAnthropic_TEMPERATURE = 0.2
Connector_AiAnthropic_MAX_TOKENS = 2000
# Agent Mail configuration
Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_CLIENT_SECRET = Kxf8Q~2lJIteZ~JaI32kMf1lfaWKATqxXiNiFbzV
Service_MSFT_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8wTnBIVXktVWJLTEdLSDd1MENKejQ2bzdCTUlTQ1ZELVJfSGhaeExkMjQ4N1dNVnhjZjRTMl83dlBqeEJCMHVabVpZVlQxRjhjQkRiOHdpMUNaODJqN0UtYW9GallJekY0U2RVZHpORXg4dThuc01uMy11ZGtDb01BQXc3TlE1ZXBjaU4=
Service_MSFT_TENANT_ID = common
# Google Service configuration
Service_GOOGLE_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_CLIENT_SECRET = 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
Service_GOOGLE_CLIENT_SECRET = INT_ENC:Z0FBQUFBQm8wTnBJeVpuNWVraERfUFBaT3BDRVk0T21KcGdrYU9zNGNyRkljNDR2TnB6R291VGJJM3d4RnBHTVVXYTRCT1F1RGFRYnNTX0xTLXFqVHVHTnN0bG9LeHdEbFpZcUNIMXFWY0dJYko4U3FNSk5vUnY2ZWRWWFJLUjR5WkJrZmpMU0pxNGI=
# Tavily Web Search configuration
Connector_WebTavily_API_KEY_SECRET = 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
Connector_WebTavily_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8wTnBJV1BlRS1UaTZmZkVYZ2hQU0lBRXVEbDl3N3BFNVI4MlBsN2JRSHdrYV95SC1vdk1pMnQzNGRaQThrRy1HcEJyT1Y2OXdQcmw2Yk9KQ3RDRzRpamx2cFpkYkN5SjkzNVVmZnVaOWJnN0MwTGZMcVdRdU1jY2kwVGhNMXZQUG9kajA=
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = {
"type": "service_account",
"project_id": "poweronid",
"private_key_id": "88db66e4248326e9baeac4231bc196fd46a9a441",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTnJuxA+xBL3LA\nPgFILYCsGuppkkdO6d153Q36f2jTj6zpH3OhKMVsaaTBknG2o2+D0Whlk6Yh5rOw\nkWzpMC3y81leRLm5kucERMkBUgd2GL4v16k6m+QGuC3BFlt/XeyuckJNW0V6v/Dy\n3+bSYM7/5o1ftPNWJeAIEWoE/V4wKCYde8RE4Vp1LO5YwhgcM4rRuPmF2OhekpA+\npteYwkY/8/gTTRpZIc8OTsBYRbaMwsjoDj5riuL3boVtkwZwKRb+ZLvupXeU7Ds7\n1305odTcZUwnImHiHfuq83ZJViQiLRNhUAFnQIXPrYLwEpCmzRBGzYHaRlb69ga/\nzqUbKnclAgMBAAECggEAH6W9qHehubioPMAJM7Y6bC2KU/JLNS4csBZd+idb52gG\nwBwIEFjR+H4ZjymhAA4+pe7c4h7MKyh0RI/l7eoFX98Cb+rEq/r1udm1BhGH3s2h\n2UiI8qRQh1YRjF2/nrN5VjhDBOFa6W9opaopZy/l8AzsT8f21zIgPen8z8o6GpFg\n64fJFcbqCGk2ykN2+x2pIOT04tmCszrfbXZP8LEs4xrUB/XwlHL1vT/M3EWIKbnj\njDaIMjw7q/KRgNUvmKS6SU9b3fnOLcQCz9f5cKdiWACKIU/UvuiWhWJ9ou6BWLWU\nva1A6Fi4XJjhW7s3po58/ioQfl0A9p/L92lGg4ST8QKBgQDx8LIM1g0dh9Ql6LmH\nBUGCOewNNXTs+y3ZznUfvVMoyyZK5w/pzeUvkmOwzbRGnZJ9WyCghq8aezyEpo2D\nPL7Odf988IeHmvhyZIM4PLJYgDvSwGXyf/gh6gJkf/4wpx+tx/yQYNBm3Rht7sA0\npSaLehK0E0kW1uyBzHGKgyQOhwKBgQDf6LiZ7hSQqh54vIU1XMDRth0UOo/s/HGi\nDoij29KjmHjLkm8vOlCo83e79X0WhcnyB5kM7nWFegwcM1PJ0Dl8gidUuTlOVDtM\n5u2AaxDoyXAUL457U5dGFAIW+R653ZDkzMfCglacP8HixXEyIpL1cTLqiCAgzszS\nLcSWwoAr8wKBgQC4CGm3X97sFpTmHSd6sCHLaDnJNl9xoAKZifUHpqCqCBVhpm8x\nXp+11vmj1GULzfJPDlE8Khbp4tH+6R39tOhC7fjgVaoSGWxgv1odHfZfYXOf9R/X\nHUZmrbUSM1XsNkPfkZ7pR+teQ1HA1Xo40WMHd1zgw0a2a9fNR/EZ9nUn4wKBgGaK\nUEgGNRrPHadTRnnaoV8o1IZYD2OLdIqvtzm7SOqsv90SkaKCRUAqR5InaYKwAHy7\nqAa5Cc73xqX/h4arujff7x0ouiq5/nJIa0ndPmAtKAvGf6zQ6j0ompBkxAKAioON\nmInmYL2roSI2I5G/LagDkDrB3lzH+Brk5NvZ9RKrAoGAGox462GGGb/NbGdDkahN\ndifzYYvq4FPiWFFo0ynKAulxCBWLXO/N45XNuAyen433d8eREcAYz1Dzax44+MdQ\nHo9dU7YcZvFyt6iZsYeQF8dluHui3vzMpUe0KbqpZC5KMOSw53ZdNIwzo8NTAK59\n+uv3dHGj7sS8fhDo3yCifzc=\n-----END PRIVATE KEY-----\n",
"client_email": "poweron-voice-services@poweronid.iam.gserviceaccount.com",
"client_id": "116641749406798186404",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/poweron-voice-services%40poweronid.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
Connector_GoogleSpeech_API_KEY_SECRET = INT_ENC:Z0FBQUFBQm8wTnBJSDh5aW9CNE04dDVsYnBUNWdGUWhDMmFlNmY2bnl5X1llVnNZV3VGakI5RFFNYVprYm5mU0F6TVZ5NDZkYlVhMGpzM0RPNGFmNDdvV1c0Y2hUQnowYzRmREhwRk5fMVVnejlGR0Y3V1pVemtFbEZEeTFEOUptbThaSHJJeGtwWGZZQ0VLYkpaTGRXMVFxX0hRX2treG1ES2VheTdsR1U3eUxYV0xPbzExSDZzOHBQR0FSdGh2V0hXRFpRLW52ZlJyMTBDR1VkVkh4VU52MWVwMDdxYlBfbjlMeTd5M0FIVGtaT1c3WmpwZjh6Skp2djB2cXM5NkdOS29ONzdaNGk0WlpzTlJxU2cwQWxTd25XYmllbjJXemQxY0ljZkZqZFV3MEhucXNfYUR3T3diWEFyLS1WQmRiZEJXbERuQXhXanZQUDBJZFphZGk5aHFTQVRkM3B5QllYZ0Q0V19VRlRtVThEb09TWGFHVHRKc0R3eGRoYWpkT0xRbXhGb0pFYUk3MXBGekV6WDdzekMtaU1JNlNaaXdQa19keUotSDJkZDNQTVpZQjlxLWhwRWIta05YR0sxTXRVS1ZLaHRJM1IwUTMtQlUtbHU4dmVfQjdsY1Y3ODFSZXBiQUJIdFNrR3dGelkzWjhQaXR1NlFIYm1KMFVNMmlMcGQtRE1zNmx0ZTRuVUhVRUFuNUEwTnNNSTBnaTRtaVNOT0lLQTR3U01SOHNjZkdOQ2VXQXBuZ3k3Q3NjbDh1dU5fWXVkN0pvNmxZWlQyaVVLNEFEN3dxRkV5NUU1dG5kdGxieXo3WGhIalAzWjQ0TFNLRFFVZkJFRUNjQW1xNWdUUHRTOG0xVklmd09NdGd4SURxdmI2UXU3U01PVDZSM0lhbDFQUjZobkl3VC10eThuV3BSV0l3Nkw5X1dVN1RhYkdqb0ROempfQ2xjc0lQemtaSGNkMjJjR1hjN0V3NFhta2l1MVRGeG9PekdhM1V6NGpCMG5yYmZJb1BmdndyMXdpOGdSSldmRFg0UlZSX3EyTVN3ckotaGJLbU5EMG1jYnY0VmtFNk14dzdzWVloVFhWMkQxNDlmc3QwSWJZV2ZaU3J2NlNkdHlyQUVXUTNXczJZMzBua0Zmbl9MYWxSSi1QdUowQkdINWJIZFNoUlY5V2NYYjFva3A3OHZ6MEd6MGRvNkJrQjNKa21FcHI2Y3pfTWQ2TUFzUEp5M1FZazVUSUVUTnlkQ2U1RVU3OFdYYXE0S3QwT210a1c5aVlPNDhET3JBeUFHeG41MDM3aUdXVWN6TUtUMi01aGJOWGN4WTZDN29WNl9SdGRtR0gzRzJFZmhZa3p6UUpoVEMyb196aFdUVE1nTDNDSkJuN1lsSWlMWlBVS3VhclRxd1ZhWXhNUDZRa0Jlb3N5UkhiZ2pYc29ZZm55bWFZa09DZ2lvZmE4YTRoYmJJREh0ZXMtSkN3MkJBNDlJQTR1MTlEVTFQTUFJMENCQmFCeGtXYlJJVmtSUjBuNXBDa0wtVTJuVk8tVGk2dWxmeEgyV3pkOFdDU1JhRnowLU9EbWZhYWkyZVRfSDVJWDdtd0l0TF9OQ19NRi1tUnAxdHg5a0dFSDY5RzFsR1NiV0p3VG5DckNyREVjcWQ0elV1aktKNlJkNXdIVnpXY3U1bTBUbVJ5a3VucDdualg2cU1rZkEtOWpWa2tGU0puNUpNUzVaV3Y4UmhiZWhLTkdzS0h5NkxkcmNLblB2dG9lb2xYYXlqZkZiM3ZRTFVtM1VwOFFGQ1QxWFh2cUlhMFFyME5rSEJwLW5IUS1pRmNpVXVWYUR3emg0N2lDXzlFN1NnRk9ab0lLaEVvaV9FcEVfR0VBUzRkWG9KUm1sRk9DcDEtSGQzMFFXLUt3QnBpaV9fV2lQVExXSDcwc0E1ejE5SWd5c0NnQTlyWlBuVFNCeFpxN0M4M3kxaHN2RmJXekxiNy0zTEN0N2daLWpERFJ3SFFMUk11N25mRVk0MHlyMHE1d1NDbFpFaTBGSDFFcksyYVJZUEdKemtNWE9qbDBfMEpyaVVaSFdOZWpodGt4N0g5TGN1NVRQUm41cTNxRWdyTDFjd0xTSEZibUt3R0pISkpmRzlSVEYzeWNUdm5qUEZPVlVJX1d5MGpxUWNjTzFMaGlEVG9GY3RhUWZiMFpsbVR2OHNLaUs4ZkFENmNoRFRyd290a1FFZng1ckczaWxMMEVsZ3dBb0ZXdlh2YzJxTlhwZkJTV2VBandjWFB3MjJrRTR4LUUxV2lCRTdGYjNoeVhZTUx3RjFRNlFoY1VYMHRyTmlxdm9jUjAtWndLQ1RNcENDclh4TkR3ZE9tSjFOaGxIcmtWQ2ZIaHRabGNJQVI3RnZHUGtBRWt3YmpuUUhrT0VRTUxfVFZOSWZ1Z25IWUsxVDIwZEY0blQwbEdXY3hETW41UldqcW0zMUNCVHNDZkRyTGlrVlU3c0lWdFpvUzlfTGtLMGxJZUg4dUdjTU01VUtYaVQtSFBqT2F4NXhEUUlBRU1lSU54dzFhd3d2UjUxb3JXSUdQbVRyUTFlc2Z5WkNGWlNzVTc5aWllcGsxbzRmYVlFTWw4VVVtTDdkczdzQ3NFSGMzdVltSjlfY3dyNzlPaEk1cE5jdk4weDFKc3BTUXpPbDI5Y1ptblp0TGJ5UGwzVmU4VEtWUTZEQlRtemp1YnppdDdkSHpyY0c4NlZqSmI1UVBwSTJSZ1NNcEQwRTdySGRySi1XUkhWTHlaajFZSkQwc1k5NGZDZUhzRFdBZXNqSVdwU0ZsMGlNLUZPSU1OT243N0RFQ091RjVyaVRBdEUtOWlfSTdpX25laHNwVFlFU0RjWE42amhxSHlvcDlDdE13Y3JtWFNsQkZoclVFS2hFblQ3MXpxOHJsUGMwbkVmT3ZMMFZiNmpHZVRCa2k0YTJnNHM0dWpyQXJaYjU1Z1hNWGU2aU1hY3RxYWVzU1hVWWI4bjR2Y0R1MmF0NDVlc08xTmVwQ01ENThUMC1kWk9SWG9IWWFDZ1V1RGhDY0pjVEZBUGJreXh3RTRuQUlTWVR4NlVuaGozbmJVTXNzRDdrMGZaclpsb290WmFaTzF0NThITENXN1JSN25JS18yWnpJY19maVlXN21QTWE5M1FhWGJqTFQ0RTZoY3dlcEM2Q0gzQ1RtYk9jTnVkU1drVnhZc1NkMm82c25CWkpaTW5KbTd5dGJHSHhhQzR2TFk1NW9pX2pQNDdOeVhGZ0swTklYUkRZbmdsVzNabTZjWDRrX3BCMC1OT0M4R0dMTWlhQXhIMXJVT0Z0bmp2aTBFWnRDSFYySVFULXRneFpBQ0ZwdEZaTHAyeWx3bFF0Q2o5YzlfbUQ5Xy1uWjFKQXVVTE9Qa2VPNDZmQ0kzaUxBNG9EdGpyZm9VOEZhTC05V2JBMU1KTmt6RlY5aUpILVE5bExnVkdCcDlLZExBeHRSWWxQS1pkcE9BWjdXeV8tb1ltcTVoalQwLTl5Mk5GanphVGxKUzlJUjk0Y1g2QXBSRzBSNkkzTjg3bTFlYU5XbE0wYkNacC05bzVFN3F4NXdkUFBqQU9QYWVtdUM1MWtNR1RzbzBQVHJGR1NzTEV5TzdCUXRmcXJuTUVCVV9QbXFXcG5QdmJfQWlVMXRvU1hOakpHandKbnFhZTlqUWtWbXg3MzFpS2I5SlJzN0dQN0JuaGNKSWtVX0Vxc1FPUk4zMzVTMVIzSWJYcUVYU3Jldm01bHpleU90MlU3XzhhVWI3a3pqajRjd0g3YVppY3MxNUdKU1lUZFhyUnZDYXJWcnltQ2tadEFxdG92aUI3MU96WnFBdXdLRWZkMFVtc0N0SDVJc0RYdjhJZVhVWXZfb2VDY3NxVEdZc05tRWNVZTRUVHphT1RyWlRoX25iNXlLX0pDeXZrbXR1VkxnUTlZOVpUak5GYzVUODZvVmtKNHo2QUg3b2pSZmNhY1lUUVA3WXktMjYxWjBiTE5XeWJlQ0VhY2VyVnRxd3hvUWxtSjhCSGNkOHFLRTF4QmpLc0FoYzg0b2xRZXZpVTZ5TENXNXdiTEZNV01nSXpPR2U4ZlZzXzZZeGdmSG5kN2hLeDR6WU0zWExxUXJBcXR6NmdZb0ZiZHRPSFVuRUVyZV9zOUJsN09GWE5YM25FRlNoLTlNNWVMcTctb2l1Mzdyb0Y5TDU1YjdnQVUwSFoxXzF0VFh3TGktbFl2bVQ3NzdkRHV3SlNfaEZjY1ItYUk0OHhLRk9rUXdqNUVTdGNBc1ZCY1pFQ0N0WE45MUEwUlpwejdrcDNBPT0=

View file

@ -10,25 +10,25 @@ APP_KEY_SYSVAR = CONFIG_KEY
DB_APP_HOST=gateway-prod-server.postgres.database.azure.com
DB_APP_DATABASE=poweron_app
DB_APP_USER=gzxxmcrdhn
DB_APP_PASSWORD_SECRET=prod_password_very_secure.2025
DB_APP_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMd0FKLUpzaTdYT0Zia2V3VExPSktfTUx6RmRDc1hobjhYamxyMTkxakhDeGVHRTA3TmVoNC1Mamh0elFiV0h5MnA3YmpheXRzLVdhN2Ytb2R4a1NiSWY0RlFQMXlJU2hUMFY1RGJ1dEdRTFE9
DB_APP_PORT=5432
# PostgreSQL Storage (new)
DB_CHAT_HOST=gateway-prod-server.postgres.database.azure.com
DB_CHAT_DATABASE=poweron_chat
DB_CHAT_USER=gzxxmcrdhn
DB_CHAT_PASSWORD_SECRET=prod_password_very_secure.2025
DB_CHAT_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMM3p1TEY3VTQxT0xrbW9fbHFJLXNDZHJUOVBSUHhhdURpMi1EZTZIaXQ0M1V5ZUZFQVhjSGF5SUVzTDNrWW11UlNQQVhwNEU0al9yZXQxSnRIU1U0akRDbFVIUHVvUV9SMkFkaEFGR1ZVUjA9
DB_CHAT_PORT=5432
# PostgreSQL Storage (new)
DB_MANAGEMENT_HOST=gateway-prod-server.postgres.database.azure.com
DB_MANAGEMENT_DATABASE=poweron_management
DB_MANAGEMENT_USER=gzxxmcrdhn
DB_MANAGEMENT_PASSWORD_SECRET=prod_password_very_secure.2025
DB_MANAGEMENT_PASSWORD_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMRDJRY19uM1hTNC1mMzhVaGNtamtScGpVYTY3RUdBTlpTTDdrUF9PdF84WkFSakRoX0VEcGhwanBPSU9OUGJNWXJDblVUS0o0Y0FBd0hMejUyTXFJTFVCaUJmTkpVYVQzWXFRSDV2d1lENHM9
DB_MANAGEMENT_PORT=5432
# Security Configuration
APP_JWT_KEY_SECRET=rotated_jwt_secret_2025_09_17_prod_e1a9c4d7-6b8f-4f2e-9c1a-7e3d2a1b9c5f
APP_JWT_KEY_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMX2lyNHVQVVkzamE1eURGMkRoVmhJTTVSTEQ1c3E4XzlucExfdUNxTHNwazB2X1h4YzdUeDhsYWNCbUZ5VjJNVTZDYlY2dGhreTg5UGV2Z3A4X1FTc094XzhxdWRNSzBXd20yY3pFNkpUYzhaeml5ME9OMjFkNjZMQkdvczZnWTVYX09fR0RYQXhpVHFPQnA2cWh1T3pqTFVieXpHV1hlUjVQdWRCSEc1bk1ZPQ==
APP_TOKEN_EXPIRY=300
# CORS Configuration
@ -50,41 +50,29 @@ Service_GOOGLE_REDIRECT_URI = https://gateway-prod.poweron-center.net/api/google
# OpenAI configuration
Connector_AiOpenai_API_URL = https://api.openai.com/v1/chat/completions
Connector_AiOpenai_API_SECRET = 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
Connector_AiOpenai_API_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMSlhwejcyRl9EUWpPX3M5bnI3QTNiRDd1QXVaVkFCczBzeUczcHhyenJvRDN0SDZGaHp6dGJqNjNiLW9oTjJPZGV1b0VxWElfT29jQ19vNWF4aG11bkRlS1JMa1VoeG82VWVmWkV0VDZUWTFmcXZXYUh6ZWs0bEswNXhhZ1ZEU1JNYk1jU0p3YVZkZmZVWmF4dURDcGR3PT0=
Connector_AiOpenai_MODEL_NAME = gpt-4o
Connector_AiOpenai_TEMPERATURE = 0.2
Connector_AiOpenai_MAX_TOKENS = 2000
# Anthropic configuration
Connector_AiAnthropic_API_URL = https://api.anthropic.com/v1/messages
Connector_AiAnthropic_API_SECRET = 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
Connector_AiAnthropic_API_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMaEFnaHBDYndpTkZJSFp5OGdmY2xtNDZEZmFmbk1rUUQ2STZCQlprMjRhY3BLdkhTWWdDRlIzcm94NE5LZ2dCdlNkdWpkVVk2QnIzTzQ5TGEtX2p6a2kzeF9PR3QtNWs4aWFKX1ozUTNYT09sMkJNb1JMRk1vbTE0U0Y2eU1SUjhwY3Z2TWIyU2d4Nk1iS2d0YkRKUm0wNjNEbWNxYTg3SGNnU3FMSzVtYjhLVnhxbXd1SmZyam9QSGtna1dkSGlpeENEREZQck1tZk4tTkJvTERTcjZSdz09
Connector_AiAnthropic_MODEL_NAME = claude-3-5-sonnet-20241022
Connector_AiAnthropic_TEMPERATURE = 0.2
Connector_AiAnthropic_MAX_TOKENS = 2000
# Agent Mail configuration
Service_MSFT_CLIENT_ID = c7e7112d-61dc-4f3a-8cd3-08cc4cd7504c
Service_MSFT_CLIENT_SECRET = Kxf8Q~2lJIteZ~JaI32kMf1lfaWKATqxXiNiFbzV
Service_MSFT_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMMXV1OE5qODFrcGJqVEt2Zlk1TkFyQ3VoMzVad21UcTgwSXJqRjdiWmdsS0J3VWRBWWg4WWllNzE5X21ubGItMl96b0hZYTlXbVBkTmVhQVRadGlnWUlWQWdOZUV2U0pDSDdiWEhMNHJQUVllYzFpWFNJUnY0M0FpZ1ZWcExyWmk=
Service_MSFT_TENANT_ID = common
# Google Service configuration
Service_GOOGLE_CLIENT_ID = 354925410565-aqs2b2qaiqmm73qpjnel6al8eid78uvg.apps.googleusercontent.com
Service_GOOGLE_CLIENT_SECRET = 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
Service_GOOGLE_CLIENT_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBMYmNCSXM5cnRBVUxlYm83VG11MlBGZHhiV2hWOWxWYk5XRk1hSmhsTGdsX2dHSGhxYk5FWEpEbXdQM3hCNE1nRjZHNjlDb0RMWTIwb2pqczdocjFkSWxfYWlLOU9KbmtUcTl1SmZJZUh2V1RwM2kzVkZhRFIyTERsaThXYS1OVFk=
# Tavily Web Search configuration
Connector_WebTavily_API_KEY_SECRET = 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
Connector_WebTavily_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBNVU9JZEcwUWFuQ0lfRElGdDRhSFJDNVVBNUhBVzVKQlhBZXNsUDluRXYyV1NuaWw3eEJMdnhscGNZNW5KVmgtMzNfSGRfX1RMZlB5SmtHSzNTMC1RUlp1c2dqOWhSVnhuVUVGVUlaak16ZjlpWW00OFVIRFU1aEZXYzNaN3VNS1I=
# Google Cloud Speech Services configuration
Connector_GoogleSpeech_API_KEY_SECRET = {
"type": "service_account",
"project_id": "poweronid",
"private_key_id": "88db66e4248326e9baeac4231bc196fd46a9a441",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTnJuxA+xBL3LA\nPgFILYCsGuppkkdO6d153Q36f2jTj6zpH3OhKMVsaaTBknG2o2+D0Whlk6Yh5rOw\nkWzpMC3y81leRLm5kucERMkBUgd2GL4v16k6m+QGuC3BFlt/XeyuckJNW0V6v/Dy\n3+bSYM7/5o1ftPNWJeAIEWoE/V4wKCYde8RE4Vp1LO5YwhgcM4rRuPmF2OhekpA+\npteYwkY/8/gTTRpZIc8OTsBYRbaMwsjoDj5riuL3boVtkwZwKRb+ZLvupXeU7Ds7\n1305odTcZUwnImHiHfuq83ZJViQiLRNhUAFnQIXPrYLwEpCmzRBGzYHaRlb69ga/\nzqUbKnclAgMBAAECggEAH6W9qHehubioPMAJM7Y6bC2KU/JLNS4csBZd+idb52gG\nwBwIEFjR+H4ZjymhAA4+pe7c4h7MKyh0RI/l7eoFX98Cb+rEq/r1udm1BhGH3s2h\n2UiI8qRQh1YRjF2/nrN5VjhDBOFa6W9opaopZy/l8AzsT8f21zIgPen8z8o6GpFg\n64fJFcbqCGk2ykN2+x2pIOT04tmCszrfbXZP8LEs4xrUB/XwlHL1vT/M3EWIKbnj\njDaIMjw7q/KRgNUvmKS6SU9b3fnOLcQCz9f5cKdiWACKIU/UvuiWhWJ9ou6BWLWU\nva1A6Fi4XJjhW7s3po58/ioQfl0A9p/L92lGg4ST8QKBgQDx8LIM1g0dh9Ql6LmH\nBUGCOewNNXTs+y3ZznUfvVMoyyZK5w/pzeUvkmOwzbRGnZJ9WyCghq8aezyEpo2D\nPL7Odf988IeHmvhyZIM4PLJYgDvSwGXyf/gh6gJkf/4wpx+tx/yQYNBm3Rht7sA0\npSaLehK0E0kW1uyBzHGKgyQOhwKBgQDf6LiZ7hSQqh54vIU1XMDRth0UOo/s/HGi\nDoij29KjmHjLkm8vOlCo83e79X0WhcnyB5kM7nWFegwcM1PJ0Dl8gidUuTlOVDtM\n5u2AaxDoyXAUL457U5dGFAIW+R653ZDkzMfCglacP8HixXEyIpL1cTLqiCAgzszS\nLcSWwoAr8wKBgQC4CGm3X97sFpTmHSd6sCHLaDnJNl9xoAKZifUHpqCqCBVhpm8x\nXp+11vmj1GULzfJPDlE8Khbp4tH+6R39tOhC7fjgVaoSGWxgv1odHfZfYXOf9R/X\nHUZmrbUSM1XsNkPfkZ7pR+teQ1HA1Xo40WMHd1zgw0a2a9fNR/EZ9nUn4wKBgGaK\nUEgGNRrPHadTRnnaoV8o1IZYD2OLdIqvtzm7SOqsv90SkaKCRUAqR5InaYKwAHy7\nqAa5Cc73xqX/h4arujff7x0ouiq5/nJIa0ndPmAtKAvGf6zQ6j0ompBkxAKAioON\nmInmYL2roSI2I5G/LagDkDrB3lzH+Brk5NvZ9RKrAoGAGox462GGGb/NbGdDkahN\ndifzYYvq4FPiWFFo0ynKAulxCBWLXO/N45XNuAyen433d8eREcAYz1Dzax44+MdQ\nHo9dU7YcZvFyt6iZsYeQF8dluHui3vzMpUe0KbqpZC5KMOSw53ZdNIwzo8NTAK59\n+uv3dHGj7sS8fhDo3yCifzc=\n-----END PRIVATE KEY-----\n",
"client_email": "poweron-voice-services@poweronid.iam.gserviceaccount.com",
"client_id": "116641749406798186404",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/poweron-voice-services%40poweronid.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
Connector_GoogleSpeech_API_KEY_SECRET = PROD_ENC:Z0FBQUFBQm8wTnBNR2pBeUR6NmFTcFZkQmZnSmlweEpfQ1RSNnFobHZ2N0Z5Q1luRVJWSmx1WFJTMlloX2hROVQ2TjlvRTJQWjVuT1F4WjJmUVdCbTEyYTQxY2tjSXA2S2Q2NTN6RnRiQmxTTWdBeC1GdnNZUV9KWU1IQVFaQnc2VmJFSHVPVDFhaF9VYUJjWS1MRTlNSEkzUl9lTDZjX016MHZKSVE0bUNjZ3Q3Ni0zOXNfMmQxYUhTcnRzSDdQSUlyUExjUHpYQTF2cG9CS2dZajJLUzZUS09JeGRDRkdPd3Y4VFhWaS1DY3FOc0hQWXhNNVc1LTI4RjdKYjktWXc1X3hVUmg5VnhQX3BuQzJzZ0ZnbUVLNHZHWTRqNjI3VDlrNDlMLWNFVl9sVFd5cm0xbjlXa3VMOEthdXdWTkFPR2ZjQm5ReGNqbFN4NTg5NTVqcmlETGoteWhqLVEwSTNBQTN0Y2ptc0JBd1BabjUwaml0NVllOF90ejhObkR0VHdIU28yRmY1QWk1VktURW1DNWZPR2FnQ2dqS3lWSU8yT3ZDN3J5Y2FzRmJOOFNTWGZhUXNzbmFKOXdxakRfekFyZEFSQlF1QnJIMF9idktTQlp5MWI3eklrOHJPUmVxOFRyWmJNSGVNXzhDSTkwd29tSTJhTVM0T3lMYWhQZzEyd0RYX21NVVFTMm5JMHVpOFFfSHdkc3RZX1A2czVtRGwzU2RZUTVYaEp1TGpfejNwLXdZY0pZQmotOXVGbGRxcnFNdU9XUEZKODJvWnA5TE1mYktjUGUtRkJJbHZuTF81em0wVTdVWi1QRGk1dG45cWR6QkZmRHd0WWRqZE9xR0FCRXktenhLYmNpY2pfYTU0bUtPYk5KWFVRc1E2M0dsSWVFWWJucXc4QnBBanRjZk5GbzVKXzVMX0ZzTFZNYUZXTm8zNjMtb1dEU0VkTmtaR2xPZmZXam5qTVIzMXJpeG5raXVJOHdBZXhXeTA1SjBJU0xHZjZubDk1Z2k3ZFltdzRQbzZZNlJfcHVHWjRzWXJIVlAyRVU1eVVYVzd0R0JoaU5DWEF6ZkdDeGZXdDFiMXpiTkEtTUtuMEt6ZFJyMG0yN2NFbDFna2V6dEl5cmJkS2liOHV5OUhBVGJRX29vVmk5VnptUEhoT01oOTlHeHZaUTVCbTQ3Z1ZkY3J0NnpPcl9JLXVYOXBCZVh6blNSVFJFVUJBT0lOckNyQjI4SlpBV283TUtBYWpZMWVpV1p2czQxZHlBYnJ6d2JUQjZ1WXRGdEdkYy1keHFjRG51N0NSN1c1ZEttTXRfSGZEbzhsV0p3aW1rbHJpS0pEaV9IYTRiVi1WLWF5TUJGMnhVZ3c4dUxiY2ljTXE0S0JJUWM2M21NemwtdlhxcTJta1ROYUpUTmVaLThyQWRxS2NpM0QtaVJyQVU3WWQ5V1kwdUp6dzlpbUxzekxwVGgtQl83MTdWeFoweUZLcFZwNnQ4cVdfMkw2MHBZUU1CSXJNMjk3YThpczRqWTBuZjlRSUJfMXQtdk1uZUVTMzVvNFNYekFmRFVJSG1ib3RzMHZiQXVqa0VWNzNid3RfTzNiTDJrU1VBY0xoWDV3Y21OMWpzUW5IZ25RRWZRT3ZwLVVESG8wUGNBcXFfWGZKUVZPLWVnNlRjaFRibmlrLTgwUmhRRWVNNFRUWjBjbjNHSFUydGY3NWNQSTc3NWlXY2s5U0lOQ2hUeWZiVXdVOV8wRjdFTXNzN25nSE9JMTJqNmxEMTN2U0N1TnRDWWJpMm9WM3FRbWY3bWZGY2huUURlQmdIdkRBNGhLWEl4M1hqNkotY1FBWm9xX2FJZVRBekZoaWx0R2k4eEF2T042dkV3cmVhMG42Q3NTM3dILTI3NEVadUpiUjU2cmxVeExMTlFpVzBfWmxmZVRXSU4xWmVhdTZqaVpmeWhwUjB1VE15SVFtMFhqUTVLOHd4dEVkQ2hiSk5nczJ5aUV1Vk85OW05YWJMYkdMSXRCZV9WLWxudEtUX0FKR0hDNjdMcEREMUlWMHJaV3RUcUpMLXlEU0Q2ZUhETVJycENlTjV2VktibXhTelJDcUFYWWRwV1VHbldtOFdHRlZJcDc2dzBXZGlJTnlSeU9mUzZHRzJNd25WS1FfdUpCRUd0NDdGaFoxVU41Qi1pM1ZQdHZ1THJBdkRMeVYwNGpkRWl2WWtrbnpoRWVFMzc2ME9WVUNNYXEtRnJKWnprekhxcHJVRVRTdHNlSE1ZdWFtLVRjWDJvVVFBTS01NXBjbGlNNC1Dcm1NaUdiUk1uUDFDdlJ0UFRlSkR0eDJwOWladzFxOFpoY1lUc3o1MmUzWm1MYUs3MjYzdk9KbWtrOWxDVWhlOTY5TlVEM210YnlmZXhnUzR6bTVHbi1IS3ZyX0lSc0FtOVBJV1BBeldJNDk1UUxaSW90UG5IY1hJZVhhWHQ3ZzQyd29YOU9DNEthWXdnRkx0aDB1LTBUN0VOUlBGZ2ZPLWY1UXdTQVRCRmhJQ090a2xUbzN6YXhuTmNqaWJpajR2Mm1Rd2lxelc3UjRSRm11dFBlVlRsTlRoRUtPYkJVbzh2Q2Y2MUhqZnNsb3E5cUhLV0hFRTlQVzcwVm5DZ283ekJPNDRxSi1neTZYR3E3UEdXT2kwLWVaZGxFUnlaazRHa01TSEtyRy15S3QydHBHcnlMZWx5Z2xqY1ZkTEVIMERIajl1dW5JZzY2NWVjV0JSb1NzSG5OeXNheFR6QmoybjlBeThHVkJtNHduaXlJYkVySlNYTDMzNWpneHlXeklvYWRvQzAzV1lDa2lzaU5CUEFNNVdLMEpWZ1BVbUhhNm1NX0k0ZVM1dFNFOXFaSkVuTXhFUHYtdF82bGxfcmFMMG9kTUtMOG9adWF3VFlYVnBTSjREbWRMX0pLR2ZHRHpIdGMzYW5OVUtzVGp3OXc5WDJfd0l2T0lVN2xvMjBzSHZSaWY1cDhTUmhVdDR0dWgtMGp4V2V2bDRQSC05cFdyNHBYaHF2dldwTVVNWExYZEZFQTlmaHFMald2LTdJX3lfRi1WbXhfXzRtUzNXVXBHUTBocFRST1h0akVZc1BleDc0aGxidS0zTjl1cTA1aEdWRXUzaXlNQUR4N0ctdkhISE9DSGl2amdyTFZ5bTFnakFrVndhQ0ZTYi1GcHM2TG5aU0xXUGhjMVRBaTRNYzBlcXdtYkdsQ0xwTmxmRzJpV3NnUkdMMDA5czFaUnR0R01TSmZqeHB3MGM0WkxpNzE2SXpSaFhENzh0OWU2M01JZ1pCWHhZdGN0aUJmS3lSWHNMWXhZNnVZSHlrYkFkNHlaVm1FcEwyTzRYN3dhdGItVEh6TS1NU2R6YkNhNjEwSEpwdzF1WUdtWldJQ2ZkUEhqM3VhTFpvWjhxQ2dpNlhpY0NTcTEycm9GQk9NM3Bla1F2REJRLV9ZYW1Dc19JZ1NFOW5yLUxCX0tvakE4ODhhOEh0SXA5anJjRGJ6d3dObzVfU3FYbW1kLWVCSThIWnl5TEh1OFlVTml5QTRDVlJPaU9mZTB0NjhSZjQtT0lpa1piWmwybUFQOURlOHBtQW9KdE5SZTJzR1A3MkJVS0xuZTRKSmYtcW5OT2M4Y3BWaUVYQUJTblpsR0xYckx1eklwNzh4ckpBNUxaMWlKd1dINjQ0UWUzejNPcnpMWHZHZTg4NlhyTnlhdXZSQ093QWktTnNLcU40aTBGUFFNUWFKcU1EZWRQalVxbEtIcENTWXl4NXRRLUkxYXhGeDkzY3pfcVgyUlFWa0dVVXNHU3dvOXRDZC1TYnZVS1d4SjdKT3NKTjRMbFZqUlJXSkRQQnNUM2VkbWRhaG1qMFBVQ1VxX0FNYnJmbFI1RjgxSjE4c25VNm1EOXpaanhQLXVJcXEyN2VaTExZMURzYThwdzh5NEVUY3Rab0plT0tWRU5rQmJvOTlWRlp3elRQSWd0LW5laGpQaFFtUzdZVzExSjQ3ZVowck82TjZjektjZzZ4NElGa2pIYjJHa29FR1BsSG5HZGlmd0xJU0RSUW96emZVSUNzSzNCTlNUcGNLQzVSaTRacWtOUWxwOTBLdkt5Q19PWDRONGp4bVNXZHd5ZmtHSVFfX0lCNFhrY2NVU0N1MllJOGtmcVFXQXpJRkdiX2dSVWRuakdldHZ5bEo2U21jQmVtcHpTaHlVZ0k4ZF9xemxMOU92bHllM2VVd1FPUDVRNC1DTFZ1U25yQnlrNm4yUi1qNTRtOU9OaVVYVzZ6Z28wT3lubDk1SF95Zm9rSWZsMVg5NnNDQTl1YzhRPT0=

View file

@ -598,18 +598,24 @@ class DatabaseConnector:
tables = []
try:
# Ensure connection is alive
self._ensure_connection()
if not self.connection or self.connection.closed:
logger.error("Database connection is not available")
return tables
with self.connection.cursor() as cursor:
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name NOT LIKE '_%'
ORDER BY table_name
""")
rows = cursor.fetchall()
tables = [row['table_name'] for row in rows]
except Exception as e:
logger.error(f"Error reading the database: {e}")
logger.error(f"Error reading the database {self.dbDatabase}: {e}")
return tables

View file

@ -1,4 +1,4 @@
from fastapi import APIRouter, Response, Depends, Request
from fastapi import APIRouter, Response, Depends, Request, Body
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import os
@ -6,16 +6,14 @@ import logging
from pathlib import Path as FilePath
from typing import Dict, Any, List
from fastapi import HTTPException, status
from datetime import datetime
from modules.shared.configuration import APP_CONFIG
from modules.security.auth import limiter, getCurrentUser
from modules.interfaces.interfaceAppModel import User
router = APIRouter(
prefix="",
tags=["General"],
responses={404: {"description": "Not found"}}
)
from modules.interfaces.interfaceAppObjects import getRootInterface
from modules.interfaces.interfaceChatObjects import getInterface as getChatInterface
from modules.interfaces.interfaceComponentObjects import getInterface as getComponentInterface
# Static folder setup - using absolute path from app root
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
@ -33,24 +31,79 @@ router = APIRouter(
# Mount static files
router.mount("/static", StaticFiles(directory=str(staticFolder), html=True), name="static")
def get_interface_for_database(database_name: str, currentUser: User):
"""
Get the appropriate interface based on database name.
Args:
database_name: Name of the database
currentUser: Current user for interface initialization
Returns:
Interface object for the specified database
Raises:
HTTPException: If database name is unknown or interface cannot be created
"""
# Get database names from configuration
appDbName = APP_CONFIG.get("DB_APP_DATABASE")
chatDbName = APP_CONFIG.get("DB_CHAT_DATABASE")
managementDbName = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if not appDbName:
raise HTTPException(status_code=500, detail="DB_APP_DATABASE configuration is required")
# Map database names to their corresponding interfaces
if database_name == appDbName:
return getRootInterface()
elif chatDbName and database_name == chatDbName:
return getChatInterface(currentUser)
elif managementDbName and database_name == managementDbName:
return getComponentInterface(currentUser)
else:
available_dbs = [appDbName]
if chatDbName:
available_dbs.append(chatDbName)
if managementDbName:
available_dbs.append(managementDbName)
raise HTTPException(status_code=400, detail=f"Unknown database. Available: {', '.join(available_dbs)}")
@router.get("/")
@limiter.limit("30/minute")
async def root(request: Request) -> Dict[str, str]:
"""API status endpoint"""
# Validate required configuration values
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
if not allowedOrigins:
raise HTTPException(status_code=500, detail="APP_ALLOWED_ORIGINS configuration is required")
return {
"status": "online",
"message": "Data Platform API is active",
"allowedOrigins": f"Allowed origins are {APP_CONFIG.get('APP_ALLOWED_ORIGINS')}"
"allowedOrigins": f"Allowed origins are {allowedOrigins}"
}
@router.get("/api/environment")
@limiter.limit("30/minute")
async def get_environment(request: Request, currentUser: Dict[str, Any] = Depends(getCurrentUser)) -> Dict[str, str]:
"""Get environment configuration for frontend"""
# Validate required configuration values
apiBaseUrl = APP_CONFIG.get("APP_API_URL")
if not apiBaseUrl:
raise HTTPException(status_code=500, detail="APP_API_URL configuration is required")
environment = APP_CONFIG.get("APP_ENV")
if not environment:
raise HTTPException(status_code=500, detail="APP_ENV configuration is required")
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
if not instanceLabel:
raise HTTPException(status_code=500, detail="APP_ENV_LABEL configuration is required")
return {
"apiBaseUrl": APP_CONFIG.get("APP_API_URL", ""),
"environment": APP_CONFIG.get("APP_ENV", "development"),
"instanceLabel": APP_CONFIG.get("APP_ENV_LABEL", "Development"),
"apiBaseUrl": apiBaseUrl,
"environment": environment,
"instanceLabel": instanceLabel,
# Add other environment variables the frontend might need
}
@ -63,3 +116,184 @@ async def options_route(request: Request, fullPath: str) -> Response:
@limiter.limit("30/minute")
async def favicon(request: Request) -> FileResponse:
return FileResponse(str(staticFolder / "favicon.ico"), media_type="image/x-icon")
# ----------------------
# Log Management
# ----------------------
@router.get("/api/logs/app")
@limiter.limit("10/minute")
async def download_app_log(request: Request, currentUser: User = Depends(getCurrentUser)) -> FileResponse:
"""Download the current day's application log file"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
# Get log directory from config
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR")
if not logDir:
raise HTTPException(status_code=500, detail="APP_LOGGING_LOG_DIR configuration is required")
if not os.path.isabs(logDir):
# If relative path, make it relative to the gateway directory
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logDir = os.path.join(gatewayDir, logDir)
# Get current date for log file
today = datetime.now().strftime("%Y%m%d")
logFile = os.path.join(logDir, f"log_app_{today}.log")
if not os.path.exists(logFile):
raise HTTPException(status_code=404, detail=f"Application log file for today not found: {logFile}")
return FileResponse(
path=logFile,
filename=f"log_app_{today}.log",
media_type="text/plain"
)
@router.get("/api/logs/audit")
@limiter.limit("10/minute")
async def download_audit_log(request: Request, currentUser: User = Depends(getCurrentUser)) -> FileResponse:
"""Download the current day's audit log file"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
# Get log directory from config
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR")
if not logDir:
raise HTTPException(status_code=500, detail="APP_LOGGING_LOG_DIR configuration is required")
if not os.path.isabs(logDir):
# If relative path, make it relative to the gateway directory
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
logDir = os.path.join(gatewayDir, logDir)
# Get current date for log file
today = datetime.now().strftime("%Y%m%d")
logFile = os.path.join(logDir, f"log_audit_{today}.log")
if not os.path.exists(logFile):
raise HTTPException(status_code=404, detail=f"Audit log file for today not found: {logFile}")
return FileResponse(
path=logFile,
filename=f"log_audit_{today}.log",
media_type="text/plain"
)
# ----------------------
# Database Management
# ----------------------
@router.get("/api/databases")
@limiter.limit("10/minute")
async def list_databases(request: Request, currentUser: User = Depends(getCurrentUser)) -> Dict[str, Any]:
"""List available databases"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
try:
# Get configured database names from configuration
databases = []
# App database - required configuration
appDb = APP_CONFIG.get("DB_APP_DATABASE")
if not appDb:
raise HTTPException(status_code=500, detail="DB_APP_DATABASE configuration is required")
databases.append(appDb)
# Chat database - optional configuration
chatDb = APP_CONFIG.get("DB_CHAT_DATABASE")
if chatDb and chatDb not in databases:
databases.append(chatDb)
# Management database - optional configuration
managementDb = APP_CONFIG.get("DB_MANAGEMENT_DATABASE")
if managementDb and managementDb not in databases:
databases.append(managementDb)
return {"databases": databases}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing databases: {e}")
raise HTTPException(status_code=500, detail="Failed to list databases")
@router.get("/api/databases/{database_name}/tables")
@limiter.limit("10/minute")
async def list_tables(
request: Request,
database_name: str,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""List tables in a specific database"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
try:
# Get the appropriate interface based on database name
interface = get_interface_for_database(database_name, currentUser)
# Check if interface and database connection exist
if not interface or not interface.db:
raise HTTPException(status_code=500, detail="Database interface not available")
# Get tables from database
tables = interface.db.getTables()
return {"database": database_name, "tables": tables}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error listing tables for database {database_name}: {e}")
raise HTTPException(status_code=500, detail=f"Failed to list tables for database {database_name}")
@router.post("/api/databases/{database_name}/tables/drop")
@limiter.limit("5/minute")
async def drop_table(
request: Request,
database_name: str,
currentUser: User = Depends(getCurrentUser),
payload: Dict[str, Any] = Body(...)
) -> Dict[str, Any]:
"""Drop a specific table from a database"""
# Check if user has admin privileges
if not hasattr(currentUser, 'privilege') or currentUser.privilege not in ('admin', 'sysadmin'):
raise HTTPException(status_code=403, detail="Admin privileges required")
table_name = payload.get("table")
if not table_name:
raise HTTPException(status_code=400, detail="Table name is required")
try:
# Get the appropriate interface based on database name
interface = get_interface_for_database(database_name, currentUser)
# Check if interface and database connection exist
if not interface or not interface.db:
raise HTTPException(status_code=500, detail="Database interface not available")
# Check if table exists
tables = interface.db.getTables()
if table_name not in tables:
raise HTTPException(status_code=404, detail=f"Table '{table_name}' not found in database '{database_name}'")
# Drop the table
with interface.db.connection.cursor() as cursor:
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
interface.db.connection.commit()
logger.warning(f"Admin drop_table executed by {currentUser.id}: dropped table '{table_name}' from database '{database_name}'")
return {"message": f"Table '{table_name}' dropped successfully from database '{database_name}'"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error dropping table {table_name} from database {database_name}: {e}")
if 'interface' in locals() and interface.db.connection:
interface.db.connection.rollback()
raise HTTPException(status_code=500, detail=f"Failed to drop table '{table_name}' from database '{database_name}'")

View file

@ -65,8 +65,15 @@ def get_token_status_for_connection(interface, connection_id: str) -> tuple[str,
return "none", None
current_time = get_utc_timestamp()
# Add 5 minute buffer for proactive refresh
buffer_time = 5 * 60 # 5 minutes in seconds
if expires_at <= current_time:
return "expired", expires_at
elif expires_at <= (current_time + buffer_time):
# Token expires soon - mark as active but log for proactive refresh
logger.debug(f"Token for connection {connection_id} expires soon (in {expires_at - current_time} seconds)")
return "active", expires_at
else:
return "active", expires_at
@ -89,6 +96,7 @@ async def get_connections(
"""Get all connections for the current user
SECURITY: This endpoint is secure - users can only see their own connections.
Automatically refreshes expired OAuth tokens in the background.
"""
try:
interface = getInterface(currentUser)
@ -97,6 +105,18 @@ async def get_connections(
# This prevents admin from seeing other users' connections and causing confusion
connections = interface.getUserConnections(currentUser.id)
# Perform silent token refresh for expired OAuth connections
try:
from modules.security.tokenRefreshService import token_refresh_service
refresh_result = await token_refresh_service.refresh_expired_tokens(currentUser.id)
if refresh_result.get("refreshed", 0) > 0:
logger.info(f"Silently refreshed {refresh_result['refreshed']} tokens for user {currentUser.id}")
# Re-fetch connections to get updated token status
connections = interface.getUserConnections(currentUser.id)
except Exception as e:
logger.warning(f"Silent token refresh failed for user {currentUser.id}: {str(e)}")
# Continue with original connections even if refresh fails
# Enhance each connection with token status information
enhanced_connections = []
for connection in connections:

View file

@ -17,8 +17,7 @@ from modules.security.auth import limiter, getCurrentUser
import modules.interfaces.interfaceComponentObjects as interfaceComponentObjects
from modules.interfaces.interfaceComponentModel import FileItem, FilePreview
from modules.shared.attributeUtils import getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
from modules.interfaces.interfaceAppModel import User, DataNeutraliserConfig, DataNeutralizerAttributes
from modules.features.featureNeutralizePlayground import NeutralizationService
from modules.interfaces.interfaceAppModel import User
# Configure logger
logger = logging.getLogger(__name__)
@ -365,253 +364,4 @@ async def preview_file(
detail=f"Error previewing file: {str(e)}"
)
# Data Neutralization endpoints
@router.get("/neutralization/config", response_model=DataNeutraliserConfig)
@limiter.limit("30/minute")
async def get_neutralization_config(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> DataNeutraliserConfig:
"""Get data neutralization configuration"""
try:
service = NeutralizationService(currentUser)
config = service.get_config()
if not config:
# Return default config instead of 404
return DataNeutraliserConfig(
mandateId=currentUser.mandateId,
userId=currentUser.id,
enabled=True,
namesToParse="",
sharepointSourcePath="",
sharepointTargetPath=""
)
return config
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting neutralization config: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization config: {str(e)}"
)
@router.post("/neutralization/config", response_model=DataNeutraliserConfig)
@limiter.limit("10/minute")
async def save_neutralization_config(
request: Request,
config_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> DataNeutraliserConfig:
"""Save or update data neutralization configuration"""
try:
service = NeutralizationService(currentUser)
config = service.save_config(config_data)
return config
except Exception as e:
logger.error(f"Error saving neutralization config: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error saving neutralization config: {str(e)}"
)
@router.post("/neutralization/neutralize-text", response_model=Dict[str, Any])
@limiter.limit("20/minute")
async def neutralize_text(
request: Request,
text_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Neutralize text content"""
try:
text = text_data.get("text", "")
file_id = text_data.get("fileId")
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Text content is required"
)
service = NeutralizationService(currentUser)
result = service.neutralize_text(text, file_id)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error neutralizing text: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error neutralizing text: {str(e)}"
)
@router.post("/neutralization/resolve-text", response_model=Dict[str, str])
@limiter.limit("20/minute")
async def resolve_text(
request: Request,
text_data: Dict[str, str] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, str]:
"""Resolve UIDs in neutralized text back to original text"""
try:
text = text_data.get("text", "")
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Text content is required"
)
service = NeutralizationService(currentUser)
resolved_text = service.resolve_text(text)
return {"resolved_text": resolved_text}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error resolving text: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error resolving text: {str(e)}"
)
@router.get("/neutralization/attributes", response_model=List[DataNeutralizerAttributes])
@limiter.limit("30/minute")
async def get_neutralization_attributes(
request: Request,
fileId: Optional[str] = Query(None, description="Filter by file ID"),
currentUser: User = Depends(getCurrentUser)
) -> List[DataNeutralizerAttributes]:
"""Get neutralization attributes, optionally filtered by file ID"""
try:
service = NeutralizationService(currentUser)
attributes = service.get_attributes(fileId)
return attributes
except Exception as e:
logger.error(f"Error getting neutralization attributes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization attributes: {str(e)}"
)
@router.post("/neutralization/process-sharepoint", response_model=Dict[str, Any])
@limiter.limit("5/minute")
async def process_sharepoint_files(
request: Request,
paths_data: Dict[str, str] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Process files from SharePoint source path and store neutralized files in target path"""
try:
source_path = paths_data.get("sourcePath", "")
target_path = paths_data.get("targetPath", "")
if not source_path or not target_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Both source and target paths are required"
)
service = NeutralizationService(currentUser)
result = await service.process_sharepoint_files(source_path, target_path)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error processing SharePoint files: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing SharePoint files: {str(e)}"
)
@router.post("/neutralization/batch-process", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def batch_process_files(
request: Request,
files_data: List[Dict[str, Any]] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Process multiple files for neutralization"""
try:
if not files_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Files data is required"
)
service = NeutralizationService(currentUser)
result = service.batch_neutralize_files(files_data)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error batch processing files: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error batch processing files: {str(e)}"
)
@router.get("/neutralization/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_neutralization_stats(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Get neutralization processing statistics"""
try:
service = NeutralizationService(currentUser)
stats = service.get_processing_stats()
return stats
except Exception as e:
logger.error(f"Error getting neutralization stats: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization stats: {str(e)}"
)
@router.delete("/neutralization/attributes/{fileId}", response_model=Dict[str, str])
@limiter.limit("10/minute")
async def cleanup_file_attributes(
request: Request,
fileId: str = Path(..., description="File ID to cleanup attributes for"),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, str]:
"""Clean up neutralization attributes for a specific file"""
try:
service = NeutralizationService(currentUser)
success = service.cleanup_file_attributes(fileId)
if success:
return {"message": f"Successfully cleaned up attributes for file {fileId}"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to cleanup file attributes"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error cleaning up file attributes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error cleaning up file attributes: {str(e)}"
)

View file

@ -0,0 +1,274 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Request, status, Query, Body
from typing import List, Dict, Any, Optional
import logging
# Import auth module
from modules.security.auth import limiter, getCurrentUser
# Import interfaces
from modules.interfaces.interfaceAppModel import User, DataNeutraliserConfig, DataNeutralizerAttributes
from modules.features.featureNeutralizePlayground import NeutralizationService
# Configure logger
logger = logging.getLogger(__name__)
# Create router for neutralization endpoints
router = APIRouter(
prefix="/api/neutralization",
tags=["Data Neutralisation"],
responses={
404: {"description": "Not found"},
400: {"description": "Bad request"},
401: {"description": "Unauthorized"},
403: {"description": "Forbidden"},
500: {"description": "Internal server error"}
}
)
@router.get("/config", response_model=DataNeutraliserConfig)
@limiter.limit("30/minute")
async def get_neutralization_config(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> DataNeutraliserConfig:
"""Get data neutralization configuration"""
try:
service = NeutralizationService(currentUser)
config = service.get_config()
if not config:
# Return default config instead of 404
return DataNeutraliserConfig(
mandateId=currentUser.mandateId,
userId=currentUser.id,
enabled=True,
namesToParse="",
sharepointSourcePath="",
sharepointTargetPath=""
)
return config
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting neutralization config: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization config: {str(e)}"
)
@router.post("/config", response_model=DataNeutraliserConfig)
@limiter.limit("10/minute")
async def save_neutralization_config(
request: Request,
config_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> DataNeutraliserConfig:
"""Save or update data neutralization configuration"""
try:
service = NeutralizationService(currentUser)
config = service.save_config(config_data)
return config
except Exception as e:
logger.error(f"Error saving neutralization config: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error saving neutralization config: {str(e)}"
)
@router.post("/neutralize-text", response_model=Dict[str, Any])
@limiter.limit("20/minute")
async def neutralize_text(
request: Request,
text_data: Dict[str, Any] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Neutralize text content"""
try:
text = text_data.get("text", "")
file_id = text_data.get("fileId")
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Text content is required"
)
service = NeutralizationService(currentUser)
result = service.neutralize_text(text, file_id)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error neutralizing text: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error neutralizing text: {str(e)}"
)
@router.post("/resolve-text", response_model=Dict[str, str])
@limiter.limit("20/minute")
async def resolve_text(
request: Request,
text_data: Dict[str, str] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, str]:
"""Resolve UIDs in neutralized text back to original text"""
try:
text = text_data.get("text", "")
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Text content is required"
)
service = NeutralizationService(currentUser)
resolved_text = service.resolve_text(text)
return {"resolved_text": resolved_text}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error resolving text: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error resolving text: {str(e)}"
)
@router.get("/attributes", response_model=List[DataNeutralizerAttributes])
@limiter.limit("30/minute")
async def get_neutralization_attributes(
request: Request,
fileId: Optional[str] = Query(None, description="Filter by file ID"),
currentUser: User = Depends(getCurrentUser)
) -> List[DataNeutralizerAttributes]:
"""Get neutralization attributes, optionally filtered by file ID"""
try:
service = NeutralizationService(currentUser)
attributes = service.get_attributes(fileId)
return attributes
except Exception as e:
logger.error(f"Error getting neutralization attributes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization attributes: {str(e)}"
)
@router.post("/process-sharepoint", response_model=Dict[str, Any])
@limiter.limit("5/minute")
async def process_sharepoint_files(
request: Request,
paths_data: Dict[str, str] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Process files from SharePoint source path and store neutralized files in target path"""
try:
source_path = paths_data.get("sourcePath", "")
target_path = paths_data.get("targetPath", "")
if not source_path or not target_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Both source and target paths are required"
)
service = NeutralizationService(currentUser)
result = await service.process_sharepoint_files(source_path, target_path)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error processing SharePoint files: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing SharePoint files: {str(e)}"
)
@router.post("/batch-process", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def batch_process_files(
request: Request,
files_data: List[Dict[str, Any]] = Body(...),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Process multiple files for neutralization"""
try:
if not files_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Files data is required"
)
service = NeutralizationService(currentUser)
result = service.batch_neutralize_files(files_data)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error batch processing files: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error batch processing files: {str(e)}"
)
@router.get("/stats", response_model=Dict[str, Any])
@limiter.limit("30/minute")
async def get_neutralization_stats(
request: Request,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Get neutralization processing statistics"""
try:
service = NeutralizationService(currentUser)
stats = service.get_processing_stats()
return stats
except Exception as e:
logger.error(f"Error getting neutralization stats: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting neutralization stats: {str(e)}"
)
@router.delete("/attributes/{fileId}", response_model=Dict[str, str])
@limiter.limit("10/minute")
async def cleanup_file_attributes(
request: Request,
fileId: str = Path(..., description="File ID to cleanup attributes for"),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, str]:
"""Clean up neutralization attributes for a specific file"""
try:
service = NeutralizationService(currentUser)
success = service.cleanup_file_attributes(fileId)
if success:
return {"message": f"Successfully cleaned up attributes for file {fileId}"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to cleanup file attributes"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error cleaning up file attributes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error cleaning up file attributes: {str(e)}"
)

View file

@ -132,6 +132,165 @@ async def update_user(
return updatedUser
@router.post("/{userId}/reset-password")
@limiter.limit("5/minute")
async def reset_user_password(
request: Request,
userId: str = Path(..., description="ID of the user to reset password for"),
newPassword: str = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Reset user password (Admin only)"""
try:
# Check if current user is admin
if currentUser.privilege != UserPrivilege.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only administrators can reset passwords"
)
# Get user interface
appInterface = getInterface(currentUser)
# Get target user
target_user = appInterface.getUserById(userId)
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# Validate password strength
if len(newPassword) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password must be at least 8 characters long"
)
# Reset password
success = appInterface.resetUserPassword(userId, newPassword)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to reset password"
)
# SECURITY: Automatically revoke all tokens for the user after password reset
try:
from modules.interfaces.interfaceAppModel import AuthAuthority
revoked_count = appInterface.revokeTokensByUser(
userId=userId,
authority=None, # Revoke all authorities
mandateId=None, # Revoke across all mandates
revokedBy=currentUser.id,
reason="password_reset"
)
logger.info(f"Revoked {revoked_count} tokens for user {userId} after password reset")
except Exception as e:
logger.error(f"Failed to revoke tokens after password reset for user {userId}: {str(e)}")
# Don't fail the password reset if token revocation fails
# Log password reset
try:
from modules.shared.auditLogger import audit_logger
audit_logger.log_security_event(
user_id=str(currentUser.id),
mandate_id=str(currentUser.mandateId),
action="password_reset",
details=f"Reset password for user {userId}"
)
except Exception:
pass
return {
"message": "Password reset successfully",
"user_id": userId
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error resetting password: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Password reset failed: {str(e)}"
)
@router.post("/change-password")
@limiter.limit("5/minute")
async def change_password(
request: Request,
currentPassword: str = Body(..., embed=True),
newPassword: str = Body(..., embed=True),
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Change current user's password"""
try:
# Get user interface
appInterface = getInterface(currentUser)
# Verify current password
if not appInterface.verifyPassword(currentPassword, currentUser.passwordHash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Current password is incorrect"
)
# Validate new password strength
if len(newPassword) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password must be at least 8 characters long"
)
# Change password
success = appInterface.resetUserPassword(str(currentUser.id), newPassword)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to change password"
)
# SECURITY: Automatically revoke all tokens for the user after password change
try:
from modules.interfaces.interfaceAppModel import AuthAuthority
revoked_count = appInterface.revokeTokensByUser(
userId=str(currentUser.id),
authority=None, # Revoke all authorities
mandateId=None, # Revoke across all mandates
revokedBy=currentUser.id,
reason="password_change"
)
logger.info(f"Revoked {revoked_count} tokens for user {currentUser.id} after password change")
except Exception as e:
logger.error(f"Failed to revoke tokens after password change for user {currentUser.id}: {str(e)}")
# Don't fail the password change if token revocation fails
# Log password change
try:
from modules.shared.auditLogger import audit_logger
audit_logger.log_security_event(
user_id=str(currentUser.id),
mandate_id=str(currentUser.mandateId),
action="password_change",
details="User changed their own password"
)
except Exception:
pass
return {
"message": "Password changed successfully. Please log in again with your new password."
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error changing password: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Password change failed: {str(e)}"
)
@router.delete("/{userId}", response_model=Dict[str, Any])
@limiter.limit("10/minute")
async def delete_user(

View file

@ -13,7 +13,7 @@ from jose import jwt
from pydantic import BaseModel
# Import auth modules
from modules.security.auth import createAccessToken, getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.security.auth import createAccessToken, createAccessTokenWithCookie, setRefreshTokenCookie, getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface
from modules.interfaces.interfaceAppModel import User, UserInDB, AuthAuthority, UserPrivilege, Token
from modules.shared.attributeUtils import ModelMixin
@ -38,6 +38,7 @@ router = APIRouter(
@limiter.limit("30/minute")
async def login(
request: Request,
response: Response,
formData: OAuth2PasswordRequestForm = Depends(),
) -> Dict[str, Any]:
"""Get access token for local user authentication"""
@ -90,24 +91,25 @@ async def login(
session_id = str(uuid.uuid4())
token_data["sid"] = session_id
# Create access token
access_token, expires_at = createAccessToken(token_data)
if not access_token:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create access token"
)
# Create access token with httpOnly cookie
access_token = createAccessTokenWithCookie(token_data, response)
# Create refresh token with httpOnly cookie
refresh_token = setRefreshTokenCookie(token_data, response)
# Get expiration time for response
try:
payload = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
expires_at = datetime.fromtimestamp(payload.get("exp"))
except Exception as e:
logger.error(f"Failed to decode access token: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to finalize token")
# Get user-specific interface for token operations
userInterface = getInterface(user)
# Decode JWT to get jti for DB persistence
try:
payload = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
jti = payload.get("jti")
except Exception as e:
logger.error(f"Failed to decode created JWT: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to finalize token")
# Get jti from already decoded payload
jti = payload.get("jti")
# Create token
token = Token(
@ -137,12 +139,12 @@ async def login(
# Don't fail if audit logging fails
pass
# Create response data
# Create response data (tokens are now in httpOnly cookies)
response_data = {
"type": "local_auth_success",
"access_token": access_token,
"token_data": token.dict(),
"authenticationAuthority": "local"
"message": "Login successful - tokens set in httpOnly cookies",
"authenticationAuthority": "local",
"expires_at": expires_at.isoformat()
}
return response_data
@ -253,20 +255,85 @@ async def read_user_me(
detail=f"Failed to get current user: {str(e)}"
)
@router.post("/refresh")
@limiter.limit("60/minute")
async def refresh_token(
request: Request,
response: Response,
currentUser: User = Depends(getCurrentUser)
) -> Dict[str, Any]:
"""Refresh access token using refresh token from cookie"""
try:
# Get refresh token from cookie
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
raise HTTPException(status_code=401, detail="No refresh token found")
# Validate refresh token
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "refresh":
raise HTTPException(status_code=401, detail="Invalid refresh token type")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Refresh token expired")
except jwt.JWTError:
raise HTTPException(status_code=401, detail="Invalid refresh token")
# Create new token data
token_data = {
"sub": currentUser.username,
"mandateId": str(currentUser.mandateId),
"userId": str(currentUser.id),
"authenticationAuthority": currentUser.authenticationAuthority
}
# Create new access token with cookie
access_token = createAccessTokenWithCookie(token_data, response)
# Get expiration time
try:
payload = jwt.decode(access_token, SECRET_KEY, algorithms=[ALGORITHM])
expires_at = datetime.fromtimestamp(payload.get("exp"))
except Exception as e:
logger.error(f"Failed to decode new access token: {str(e)}")
raise HTTPException(status_code=500, detail="Failed to create new token")
return {
"type": "token_refresh_success",
"message": "Token refreshed successfully",
"expires_at": expires_at.isoformat()
}
except HTTPException as e:
# If it's a 503 error (service unavailable due to missing token table), return it as-is
if e.status_code == 503:
raise
# For other HTTP exceptions, re-raise them
raise
except Exception as e:
logger.error(f"Token refresh error: {str(e)}")
raise HTTPException(status_code=500, detail="Token refresh failed")
@router.post("/logout")
@limiter.limit("30/minute")
async def logout(request: Request, currentUser: User = Depends(getCurrentUser)) -> JSONResponse:
async def logout(request: Request, response: Response, currentUser: User = Depends(getCurrentUser)) -> JSONResponse:
"""Logout from local authentication"""
try:
# Get user interface with current user context
appInterface = getInterface(currentUser)
# Read bearer token from Authorization header to obtain session id / jti
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.lower().startswith("bearer "):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing Authorization header")
raw_token = auth_header.split(" ", 1)[1].strip()
# Get token from cookie or Authorization header
token = request.cookies.get('auth_token')
if not token:
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.lower().startswith("bearer "):
token = auth_header.split(" ", 1)[1].strip()
if not token:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No token found")
try:
payload = jwt.decode(raw_token, SECRET_KEY, algorithms=[ALGORITHM])
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
session_id = payload.get("sid") or payload.get("sessionId")
jti = payload.get("jti")
except Exception as e:
@ -293,8 +360,12 @@ async def logout(request: Request, currentUser: User = Depends(getCurrentUser))
# Don't fail if audit logging fails
pass
# Clear httpOnly cookies
response.delete_cookie(key="auth_token", httponly=True, samesite="strict")
response.delete_cookie(key="refresh_token", httponly=True, samesite="strict")
return JSONResponse({
"message": "Successfully logged out",
"message": "Successfully logged out - cookies cleared",
"revokedTokens": revoked
})

View file

@ -6,8 +6,8 @@ Handles JWT-based authentication, token generation, and user context.
from datetime import datetime, timedelta, timezone
import uuid
from typing import Optional, Dict, Any, Tuple
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import OAuth2PasswordBearer
from fastapi import Depends, HTTPException, status, Request, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
import logging
from slowapi import Limiter
@ -24,8 +24,29 @@ ALGORITHM = APP_CONFIG.get("Auth_ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(APP_CONFIG.get("APP_TOKEN_EXPIRY"))
REFRESH_TOKEN_EXPIRE_DAYS = int(APP_CONFIG.get("APP_REFRESH_TOKEN_EXPIRY", "7"))
# OAuth2 Setup
oauth2Scheme = OAuth2PasswordBearer(tokenUrl="token")
# Cookie-based Authentication Setup
class CookieAuth(HTTPBearer):
"""Cookie-based authentication that checks httpOnly cookies first, then Authorization header"""
def __init__(self, auto_error: bool = True):
super().__init__(auto_error=auto_error)
async def __call__(self, request: Request) -> Optional[str]:
# 1. Check httpOnly cookie first (preferred method)
token = request.cookies.get('auth_token')
if token:
return token
# 2. Fallback to Authorization header for API calls
authorization = request.headers.get("Authorization")
if authorization and authorization.startswith("Bearer "):
return authorization.split(" ")[1]
if self.auto_error:
raise HTTPException(status_code=401, detail="Not authenticated")
return None
# Initialize cookie-based auth
cookieAuth = CookieAuth(auto_error=False)
# Rate Limiter
limiter = Limiter(key_func=get_remote_address)
@ -59,7 +80,82 @@ def createAccessToken(data: dict, expiresDelta: Optional[timedelta] = None) -> T
return encodedJwt, expire
def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
def createAccessTokenWithCookie(data: dict, response: Response, expiresDelta: Optional[timedelta] = None) -> str:
"""
Creates a JWT Access Token and sets it as an httpOnly cookie.
Args:
data: Data to encode (usually user ID or username)
response: FastAPI Response object to set cookie
expiresDelta: Validity duration of the token (optional)
Returns:
JWT Token as string
"""
access_token, expires_at = createAccessToken(data, expiresDelta)
# Set httpOnly cookie
response.set_cookie(
key="auth_token",
value=access_token,
httponly=True,
secure=True, # HTTPS only in production
samesite="strict",
max_age=int(expiresDelta.total_seconds()) if expiresDelta else ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
return access_token
def createRefreshToken(data: dict) -> Tuple[str, datetime]:
"""
Creates a JWT Refresh Token with longer expiration.
Args:
data: Data to encode (usually user ID or username)
Returns:
Tuple of (JWT Refresh Token as string, expiration datetime)
"""
toEncode = data.copy()
# Ensure a token id (jti) exists for revocation tracking
if "jti" not in toEncode or not toEncode.get("jti"):
toEncode["jti"] = str(uuid.uuid4())
# Add refresh token type
toEncode["type"] = "refresh"
expire = get_utc_now() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
toEncode.update({"exp": expire})
encodedJwt = jwt.encode(toEncode, SECRET_KEY, algorithm=ALGORITHM)
return encodedJwt, expire
def setRefreshTokenCookie(data: dict, response: Response) -> str:
"""
Creates a JWT Refresh Token and sets it as an httpOnly cookie.
Args:
data: Data to encode (usually user ID or username)
response: FastAPI Response object to set cookie
Returns:
JWT Refresh Token as string
"""
refresh_token, expires_at = createRefreshToken(data)
# Set httpOnly cookie for refresh token
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=True, # HTTPS only in production
samesite="strict",
max_age=REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60 # Days to seconds
)
return refresh_token
def _getUserBase(token: str = Depends(cookieAuth)) -> User:
"""
Extracts and validates the current user from the JWT token.
@ -138,7 +234,14 @@ def _getUserBase(token: str = Depends(oauth2Scheme)) -> User:
db_tokens = appInterface.db.getRecordset(
Token, recordFilter={"id": tokenId}
)
except Exception:
except Exception as e:
# Check if this is a table not found error (token table was deleted)
if "does not exist" in str(e).lower() or "relation" in str(e).lower():
logger.error("Token table does not exist - database may have been reset")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Authentication service temporarily unavailable. Please contact administrator."
)
db_tokens = []
if db_tokens:

96
modules/security/csrf.py Normal file
View file

@ -0,0 +1,96 @@
"""
CSRF Protection Middleware for PowerOn Gateway
This module provides CSRF protection for state-changing operations.
"""
import logging
from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Set
logger = logging.getLogger(__name__)
class CSRFMiddleware(BaseHTTPMiddleware):
"""
CSRF protection middleware that validates CSRF tokens for state-changing operations.
"""
def __init__(self, app, exempt_paths: Set[str] = None):
super().__init__(app)
# Paths that are exempt from CSRF protection
self.exempt_paths = exempt_paths or {
"/api/local/login",
"/api/local/register",
"/api/msft/login",
"/api/google/login",
"/api/msft/callback",
"/api/google/callback"
}
# State-changing HTTP methods that require CSRF protection
self.protected_methods = {"POST", "PUT", "DELETE", "PATCH"}
async def dispatch(self, request: Request, call_next):
"""
Check CSRF token for state-changing operations.
"""
# Skip CSRF check for exempt paths
if request.url.path in self.exempt_paths:
return await call_next(request)
# Skip CSRF check for non-state-changing methods
if request.method not in self.protected_methods:
return await call_next(request)
# Skip CSRF check for OPTIONS requests (CORS preflight)
if request.method == "OPTIONS":
return await call_next(request)
# Get CSRF token from header
csrf_token = request.headers.get("X-CSRF-Token")
if not csrf_token:
logger.warning(f"CSRF token missing for {request.method} {request.url.path}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="CSRF token missing"
)
# Validate CSRF token format (basic validation)
if not self._is_valid_csrf_token(csrf_token):
logger.warning(f"Invalid CSRF token format for {request.method} {request.url.path}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid CSRF token format"
)
# Additional CSRF validation could be added here:
# - Check token against session
# - Validate token expiration
# - Verify token origin
return await call_next(request)
def _is_valid_csrf_token(self, token: str) -> bool:
"""
Basic validation of CSRF token format.
Args:
token: The CSRF token to validate
Returns:
bool: True if token format is valid
"""
if not token or not isinstance(token, str):
return False
# Basic format validation (hex string, reasonable length)
if len(token) < 16 or len(token) > 64:
return False
# Check if token contains only valid hex characters
try:
int(token, 16)
return True
except ValueError:
return False

View file

@ -0,0 +1,191 @@
"""
Token Refresh Middleware for PowerOn Gateway
This middleware automatically refreshes expired OAuth tokens
when API endpoints are accessed, providing seamless user experience.
"""
import logging
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response as StarletteResponse
from typing import Callable
import asyncio
from modules.security.tokenRefreshService import token_refresh_service
logger = logging.getLogger(__name__)
class TokenRefreshMiddleware(BaseHTTPMiddleware):
"""
Middleware that automatically refreshes expired OAuth tokens
when API endpoints are accessed.
"""
def __init__(self, app, enabled: bool = True):
super().__init__(app)
self.enabled = enabled
self.refresh_endpoints = {
'/api/connections',
'/api/files',
'/api/chat',
'/api/msft',
'/api/google'
}
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""
Process request and refresh tokens if needed
"""
if not self.enabled:
return await call_next(request)
# Check if this is an endpoint that might need token refresh
if not self._should_check_tokens(request):
return await call_next(request)
# Extract user ID from request (if available)
user_id = self._extract_user_id(request)
if not user_id:
return await call_next(request)
try:
# Perform silent token refresh in background
# Don't wait for completion to avoid slowing down the request
asyncio.create_task(self._silent_refresh_tokens(user_id))
except Exception as e:
logger.warning(f"Error scheduling token refresh: {str(e)}")
# Continue with request even if refresh scheduling fails
# Process the original request
response = await call_next(request)
return response
def _should_check_tokens(self, request: Request) -> bool:
"""
Check if this request should trigger token refresh
"""
path = request.url.path
# Only check specific API endpoints
for endpoint in self.refresh_endpoints:
if path.startswith(endpoint):
return True
return False
def _extract_user_id(self, request: Request) -> str:
"""
Extract user ID from request context
"""
try:
# Try to get user from request state (set by auth middleware)
if hasattr(request.state, 'user_id'):
return request.state.user_id
# Try to get from JWT token in cookies or headers
# This is a fallback if user state is not available
return None
except Exception as e:
logger.debug(f"Could not extract user ID: {str(e)}")
return None
async def _silent_refresh_tokens(self, user_id: str) -> None:
"""
Perform silent token refresh for the user
"""
try:
logger.debug(f"Starting silent token refresh for user {user_id}")
# Refresh expired tokens
result = await token_refresh_service.refresh_expired_tokens(user_id)
if result.get("refreshed", 0) > 0:
logger.info(f"Silently refreshed {result['refreshed']} tokens for user {user_id}")
except Exception as e:
logger.error(f"Error in silent token refresh for user {user_id}: {str(e)}")
class ProactiveTokenRefreshMiddleware(BaseHTTPMiddleware):
"""
Middleware that proactively refreshes tokens before they expire
"""
def __init__(self, app, enabled: bool = True, check_interval_minutes: int = 5):
super().__init__(app)
self.enabled = enabled
self.check_interval_minutes = check_interval_minutes
self.last_check = {}
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""
Process request and check for proactive refresh needs
"""
if not self.enabled:
return await call_next(request)
# Extract user ID from request
user_id = self._extract_user_id(request)
if not user_id:
return await call_next(request)
# Check if we need to do proactive refresh
if self._should_check_proactive_refresh(user_id):
try:
# Perform proactive refresh in background
asyncio.create_task(self._proactive_refresh_tokens(user_id))
self.last_check[user_id] = get_utc_timestamp()
except Exception as e:
logger.warning(f"Error scheduling proactive refresh: {str(e)}")
# Process the original request
response = await call_next(request)
return response
def _extract_user_id(self, request: Request) -> str:
"""
Extract user ID from request context
"""
try:
if hasattr(request.state, 'user_id'):
return request.state.user_id
return None
except Exception:
return None
def _should_check_proactive_refresh(self, user_id: str) -> bool:
"""
Check if we should perform proactive refresh for this user
"""
try:
from modules.shared.timezoneUtils import get_utc_timestamp
current_time = get_utc_timestamp()
last_check = self.last_check.get(user_id, 0)
# Check every 5 minutes
return (current_time - last_check) > (self.check_interval_minutes * 60)
except Exception:
return False
async def _proactive_refresh_tokens(self, user_id: str) -> None:
"""
Perform proactive token refresh for the user
"""
try:
logger.debug(f"Starting proactive token refresh for user {user_id}")
result = await token_refresh_service.proactive_refresh(user_id)
if result.get("refreshed", 0) > 0:
logger.info(f"Proactively refreshed {result['refreshed']} tokens for user {user_id}")
except Exception as e:
logger.error(f"Error in proactive token refresh for user {user_id}: {str(e)}")
def get_utc_timestamp():
"""Get current UTC timestamp"""
from modules.shared.timezoneUtils import get_utc_timestamp as _get_utc_timestamp
return _get_utc_timestamp()

View file

@ -0,0 +1,291 @@
"""
Token Refresh Service for PowerOn Gateway
This service handles automatic token refresh for OAuth connections
when they are accessed via API calls. It runs silently in the background
to ensure users don't experience token expiration issues.
"""
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from modules.interfaces.interfaceAppObjects import getInterface
from modules.interfaces.interfaceAppModel import User, UserConnection, AuthAuthority, Token
from modules.shared.timezoneUtils import get_utc_timestamp
from modules.shared.auditLogger import audit_logger
logger = logging.getLogger(__name__)
class TokenRefreshService:
"""Service for automatic token refresh operations"""
def __init__(self):
self.rate_limit_map = {} # Track refresh attempts per connection
self.max_attempts_per_hour = 3
self.refresh_window_minutes = 60
def _is_rate_limited(self, connection_id: str) -> bool:
"""Check if connection is rate limited for refresh attempts"""
now = get_utc_timestamp()
if connection_id not in self.rate_limit_map:
return False
# Remove attempts older than 1 hour
recent_attempts = [
attempt_time for attempt_time in self.rate_limit_map[connection_id]
if now - attempt_time < (self.refresh_window_minutes * 60)
]
self.rate_limit_map[connection_id] = recent_attempts
return len(recent_attempts) >= self.max_attempts_per_hour
def _record_refresh_attempt(self, connection_id: str) -> None:
"""Record a refresh attempt for rate limiting"""
now = get_utc_timestamp()
if connection_id not in self.rate_limit_map:
self.rate_limit_map[connection_id] = []
self.rate_limit_map[connection_id].append(now)
async def _refresh_google_token(self, interface, connection: UserConnection) -> bool:
"""Refresh Google OAuth token"""
try:
logger.debug(f"Refreshing Google token for connection {connection.id}")
# Get current token
current_token = interface.getConnectionToken(connection.id, auto_refresh=False)
if not current_token:
logger.warning(f"No Google token found for connection {connection.id}")
return False
# Import Google token refresh logic
from modules.security.tokenManager import TokenManager
token_manager = TokenManager()
# Attempt to refresh the token
refreshed_token = token_manager.refresh_token(current_token)
if refreshed_token:
# Save the refreshed token
interface.saveConnectionToken(refreshed_token)
# Update connection status
interface.db.recordModify(UserConnection, connection.id, {
"lastChecked": get_utc_timestamp(),
"expiresAt": refreshed_token.expiresAt
})
logger.info(f"Successfully refreshed Google token for connection {connection.id}")
# Log audit event
try:
audit_logger.log_security_event(
user_id=str(connection.userId),
mandate_id="system",
action="token_refresh",
details=f"Google token refreshed for connection {connection.id}"
)
except Exception:
pass
return True
else:
logger.warning(f"Failed to refresh Google token for connection {connection.id}")
return False
except Exception as e:
logger.error(f"Error refreshing Google token for connection {connection.id}: {str(e)}")
return False
async def _refresh_microsoft_token(self, interface, connection: UserConnection) -> bool:
"""Refresh Microsoft OAuth token"""
try:
logger.debug(f"Refreshing Microsoft token for connection {connection.id}")
# Get current token
current_token = interface.getConnectionToken(connection.id, auto_refresh=False)
if not current_token:
logger.warning(f"No Microsoft token found for connection {connection.id}")
return False
# Import Microsoft token refresh logic
from modules.security.tokenManager import TokenManager
token_manager = TokenManager()
# Attempt to refresh the token
refreshed_token = token_manager.refresh_token(current_token)
if refreshed_token:
# Save the refreshed token
interface.saveConnectionToken(refreshed_token)
# Update connection status
interface.db.recordModify(UserConnection, connection.id, {
"lastChecked": get_utc_timestamp(),
"expiresAt": refreshed_token.expiresAt
})
logger.info(f"Successfully refreshed Microsoft token for connection {connection.id}")
# Log audit event
try:
audit_logger.log_security_event(
user_id=str(connection.userId),
mandate_id="system",
action="token_refresh",
details=f"Microsoft token refreshed for connection {connection.id}"
)
except Exception:
pass
return True
else:
logger.warning(f"Failed to refresh Microsoft token for connection {connection.id}")
return False
except Exception as e:
logger.error(f"Error refreshing Microsoft token for connection {connection.id}: {str(e)}")
return False
async def refresh_expired_tokens(self, user_id: str) -> Dict[str, Any]:
"""
Refresh expired OAuth tokens for a user
Args:
user_id: User ID to refresh tokens for
Returns:
Dict with refresh results
"""
try:
logger.debug(f"Starting silent token refresh for user {user_id}")
# Get user interface
from modules.interfaces.interfaceAppObjects import getRootInterface
root_interface = getRootInterface()
# Get user connections
connections = root_interface.getUserConnections(user_id)
if not connections:
logger.debug(f"No connections found for user {user_id}")
return {"refreshed": 0, "failed": 0, "rate_limited": 0}
refreshed_count = 0
failed_count = 0
rate_limited_count = 0
# Process each connection
for connection in connections:
# Only refresh expired OAuth connections
if (connection.tokenStatus == 'expired' and
connection.authority in [AuthAuthority.GOOGLE, AuthAuthority.MSFT]):
# Check rate limiting
if self._is_rate_limited(connection.id):
logger.warning(f"Rate limited for connection {connection.id}")
rate_limited_count += 1
continue
# Record attempt
self._record_refresh_attempt(connection.id)
# Refresh based on authority
success = False
if connection.authority == AuthAuthority.GOOGLE:
success = await self._refresh_google_token(root_interface, connection)
elif connection.authority == AuthAuthority.MSFT:
success = await self._refresh_microsoft_token(root_interface, connection)
if success:
refreshed_count += 1
else:
failed_count += 1
result = {
"refreshed": refreshed_count,
"failed": failed_count,
"rate_limited": rate_limited_count
}
logger.info(f"Silent token refresh completed for user {user_id}: {result}")
return result
except Exception as e:
logger.error(f"Error during silent token refresh for user {user_id}: {str(e)}")
return {"refreshed": 0, "failed": 0, "rate_limited": 0, "error": str(e)}
async def proactive_refresh(self, user_id: str) -> Dict[str, Any]:
"""
Proactively refresh tokens that expire within 5 minutes
Args:
user_id: User ID to check tokens for
Returns:
Dict with refresh results
"""
try:
logger.debug(f"Starting proactive token refresh for user {user_id}")
# Get user interface
from modules.interfaces.interfaceAppObjects import getRootInterface
root_interface = getRootInterface()
# Get user connections
connections = root_interface.getUserConnections(user_id)
if not connections:
return {"refreshed": 0, "failed": 0, "rate_limited": 0}
refreshed_count = 0
failed_count = 0
rate_limited_count = 0
current_time = get_utc_timestamp()
five_minutes = 5 * 60 # 5 minutes in seconds
# Process each connection
for connection in connections:
# Only refresh active tokens that expire soon
if (connection.tokenStatus == 'active' and
connection.tokenExpiresAt and
connection.authority in [AuthAuthority.GOOGLE, AuthAuthority.MSFT]):
# Check if token expires within 5 minutes
time_until_expiry = connection.tokenExpiresAt - current_time
if 0 < time_until_expiry <= five_minutes:
# Check rate limiting
if self._is_rate_limited(connection.id):
logger.warning(f"Rate limited for proactive refresh of connection {connection.id}")
rate_limited_count += 1
continue
# Record attempt
self._record_refresh_attempt(connection.id)
# Refresh based on authority
success = False
if connection.authority == AuthAuthority.GOOGLE:
success = await self._refresh_google_token(root_interface, connection)
elif connection.authority == AuthAuthority.MSFT:
success = await self._refresh_microsoft_token(root_interface, connection)
if success:
refreshed_count += 1
logger.info(f"Proactively refreshed {connection.authority} token for connection {connection.id}")
else:
failed_count += 1
result = {
"refreshed": refreshed_count,
"failed": failed_count,
"rate_limited": rate_limited_count
}
if refreshed_count > 0:
logger.info(f"Proactive token refresh completed for user {user_id}: {result}")
return result
except Exception as e:
logger.error(f"Error during proactive token refresh for user {user_id}: {str(e)}")
return {"refreshed": 0, "failed": 0, "rate_limited": 0, "error": str(e)}
# Global service instance
token_refresh_service = TokenRefreshService()