fix: clickup redirect
All checks were successful
Deploy Plattform-Core INT / test (push) Successful in 1m0s
Deploy Plattform-Core INT / deploy (push) Successful in 9s

This commit is contained in:
Ida 2026-05-22 11:59:09 +02:00
parent 0f8e128eaa
commit c097b28b6c
5 changed files with 38 additions and 8 deletions

View file

@ -48,10 +48,10 @@ Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.ap
Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyUTUwNXNGaHRNaGxxbF9sdWJ3Q0xLYU5yOHB4Yk8zMDZvQ29yaEhWOE5JMENXRk5jb2ZBdzRKQ2ZTTld6ZlIxemhOYzN1VE10TjBDRWZEMXlLVWRNYjZ0VG5RZ3I3NWt0SEJzMzdsUmRzcVNmbktRNHZqTUF6a2EyUkVUSFJnZFE= Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyUTUwNXNGaHRNaGxxbF9sdWJ3Q0xLYU5yOHB4Yk8zMDZvQ29yaEhWOE5JMENXRk5jb2ZBdzRKQ2ZTTld6ZlIxemhOYzN1VE10TjBDRWZEMXlLVWRNYjZ0VG5RZ3I3NWt0SEJzMzdsUmRzcVNmbktRNHZqTUF6a2EyUkVUSFJnZFE=
Service_GOOGLE_DATA_REDIRECT_URI = https://api-int.poweron.swiss/api/google/auth/connect/callback Service_GOOGLE_DATA_REDIRECT_URI = https://api-int.poweron.swiss/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly. # ClickUp OAuth — same app as gateway-int; add https://api-int.poweron.swiss as second redirect in ClickUp (root URL, no path).
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4 Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = CZECD706WLSX6UV13YI4ACNW50ADZHHXDAJALHE0YE030QFSI6Y9HP4Y61JT7CF0 Service_CLICKUP_CLIENT_SECRET = CZECD706WLSX6UV13YI4ACNW50ADZHHXDAJALHE0YE030QFSI6Y9HP4Y61JT7CF0
Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss/api/clickup/auth/connect/callback Service_CLICKUP_OAUTH_REDIRECT_URI = https://api-int.poweron.swiss
# Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI. # Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI.

View file

@ -48,10 +48,11 @@ Service_GOOGLE_DATA_CLIENT_ID = 813678306829-3f23dnf1cs4aaftubjfickt46tlmkgjm.ap
Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyV1FRVjF0c0d3d0dyWU1TdW9HdXVkdHdsVWZKYTJjbGZPRDhMRjA2M0FkaUZIVmhIUmFKNjg2ekFodHd6NG80VTI3TC1icW1LZ01jWVZuQ1pKRm5nMW5UREJEaGp2Wl9oRDRCSmZVT0JpTnkwXzgwY0pkV29yczQ5akF2d1ZGcVY= Service_GOOGLE_DATA_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnFBa1kyV1FRVjF0c0d3d0dyWU1TdW9HdXVkdHdsVWZKYTJjbGZPRDhMRjA2M0FkaUZIVmhIUmFKNjg2ekFodHd6NG80VTI3TC1icW1LZ01jWVZuQ1pKRm5nMW5UREJEaGp2Wl9oRDRCSmZVT0JpTnkwXzgwY0pkV29yczQ5akF2d1ZGcVY=
Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/connect/callback Service_GOOGLE_DATA_REDIRECT_URI = https://gateway-int.poweron.swiss/api/google/auth/connect/callback
# ClickUp OAuth (Verbindungen / automation). Create an app in ClickUp: Settings → Apps → API; set redirect URL to Service_CLICKUP_OAUTH_REDIRECT_URI exactly. # ClickUp OAuth — redirect URL must match ClickUp app exactly (often API root only).
# OAuth lands on /?code=&state=; gateway forwards to /api/clickup/auth/connect/callback (routeAdmin root).
Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4 Service_CLICKUP_CLIENT_ID = O3FX3H602A30MQN4I4SBNGJLIDBD5SL4
Service_CLICKUP_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5SE1uVURMNVE3NkM4cHBKa2R2TjBnLWdpSXI5dHpKWGExZVFiUF95TFNnZ1NwLWFLdmh6eWFZTHVHYTBzU2FGRUpLYkVyM1NvZjZkWDZHN21qUER5ZVNOaGpCc3NrUGd3VnFTclF3OW1nUlVuWXQ1UVhDLVpyb1BwRExOeFpDeVhtbEhDVnd4TVdpbzNBNk5QQWFPdjdza0xBWGxFY1E3WFpCSUlNa1l4RDlBPQ== Service_CLICKUP_CLIENT_SECRET = INT_ENC:Z0FBQUFBQnB5dkd5SE1uVURMNVE3NkM4cHBKa2R2TjBnLWdpSXI5dHpKWGExZVFiUF95TFNnZ1NwLWFLdmh6eWFZTHVHYTBzU2FGRUpLYkVyM1NvZjZkWDZHN21qUER5ZVNOaGpCc3NrUGd3VnFTclF3OW1nUlVuWXQ1UVhDLVpyb1BwRExOeFpDeVhtbEhDVnd4TVdpbzNBNk5QQWFPdjdza0xBWGxFY1E3WFpCSUlNa1l4RDlBPQ==
Service_CLICKUP_OAUTH_REDIRECT_URI = https://gateway-int.poweron.swiss/api/clickup/auth/connect/callback Service_CLICKUP_OAUTH_REDIRECT_URI = https://gateway-int.poweron.swiss
# Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI. # Infomaniak: no OAuth client. Users paste a Personal Access Token (kdrive + mail) per UI.

View file

@ -29,6 +29,25 @@ _msg = apiRouteContext("oauthConnectTicket")
_CONNECT_TICKET_TTL_SEC = 600 _CONNECT_TICKET_TTL_SEC = 600
# OAuth providers sometimes redirect to the API root if the app redirect URL omits the path.
OAUTH_FLOW_CALLBACK_PATHS: Dict[str, str] = {
"clickup_connect": "/api/clickup/auth/connect/callback",
"msft_connect": "/api/msft/auth/connect/callback",
"google_connect": "/api/google/auth/connect/callback",
}
def oauth_callback_redirect_path(state: str) -> str | None:
"""Map connect-ticket JWT (ClickUp ``state`` param) to the correct callback route."""
try:
data = jose_jwt.decode(state, SECRET_KEY, algorithms=[ALGORITHM])
except JWTError:
return None
flow = data.get("flow")
if not isinstance(flow, str):
return None
return OAUTH_FLOW_CALLBACK_PATHS.get(flow)
def issue_connect_ticket(flow: str, connection_id: str, user_id: str) -> str: def issue_connect_ticket(flow: str, connection_id: str, user_id: str) -> str:
"""Issue a short-lived JWT for starting a data-connection OAuth popup.""" """Issue a short-lived JWT for starting a data-connection OAuth popup."""

View file

@ -1,7 +1,7 @@
# Copyright (c) 2025 Patrick Motsch # Copyright (c) 2025 Patrick Motsch
# All rights reserved. # All rights reserved.
from fastapi import APIRouter, Response, Depends, Request, Body from fastapi import APIRouter, Response, Depends, Request, Body
from fastapi.responses import FileResponse from fastapi.responses import FileResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import os import os
import logging import logging
@ -11,6 +11,7 @@ from fastapi import HTTPException, status
from modules.shared.configuration import APP_CONFIG from modules.shared.configuration import APP_CONFIG
from modules.auth import limiter, getCurrentUser from modules.auth import limiter, getCurrentUser
from modules.auth.oauthConnectTicket import oauth_callback_redirect_path
from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceDbApp import getRootInterface from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.i18nRegistry import apiRouteContext from modules.shared.i18nRegistry import apiRouteContext
@ -35,8 +36,15 @@ router.mount(
@router.get("/") @router.get("/")
@limiter.limit("30/minute") @limiter.limit("30/minute")
def root(request: Request) -> Dict[str, str]: def root(request: Request):
"""API status endpoint""" """API status endpoint; forwards OAuth callbacks that land on ``/`` by mistake."""
code = request.query_params.get("code")
state = request.query_params.get("state")
if code and state:
callback_path = oauth_callback_redirect_path(state)
if callback_path:
return RedirectResponse(url=f"{callback_path}?{request.url.query}", status_code=302)
# Validate required configuration values # Validate required configuration values
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS") allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
if not allowedOrigins: if not allowedOrigins:

View file

@ -257,6 +257,8 @@ async def auth_connect_callback(
except Exception as _cbErr: except Exception as _cbErr:
logger.warning("connection.established callback failed for %s: %s", connection.id, _cbErr) logger.warning("connection.established callback failed for %s: %s", connection.id, _cbErr)
allowed = (APP_CONFIG.get("APP_ALLOWED_ORIGINS") or "").split(",")[0].strip()
post_target = allowed if allowed else "*"
return HTMLResponse( return HTMLResponse(
content=f""" content=f"""
<html> <html>
@ -273,7 +275,7 @@ async def auth_connect_callback(
lastChecked: {getUtcTimestamp()}, lastChecked: {getUtcTimestamp()},
expiresAt: {expires_at} expiresAt: {expires_at}
}} }}
}}, '*'); }}, {json.dumps(post_target)});
setTimeout(() => window.close(), 1000); setTimeout(() => window.close(), 1000);
}} else {{ }} else {{
window.close(); window.close();