fix(oauth): Auto-refresh expired tokens on integrations page load
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Previously, the integrations page only read token status from DB without
attempting refresh, causing "Token wygasł" after 1h of inactivity despite
valid refresh_token. Now get_connected_services() auto-refreshes expired
tokens on page load, matching the behavior already present in audit flows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-09 09:15:30 +01:00
parent 30ef2f554b
commit fd04349c04
2 changed files with 51 additions and 12 deletions

View File

@ -197,6 +197,27 @@ class OAuthService:
logger.error(f"Token refresh error for {provider}: {e}") logger.error(f"Token refresh error for {provider}: {e}")
return None return None
def _try_refresh_token(self, db, token) -> bool:
"""Attempt to refresh an expired token in-place. Returns True on success."""
if not token.refresh_token:
return False
try:
new_data = self.refresh_access_token(token.provider, token.refresh_token)
if new_data and new_data.get('access_token'):
token.access_token = new_data['access_token']
expires_in = new_data.get('expires_in')
if expires_in:
token.expires_at = datetime.now() + timedelta(seconds=int(expires_in))
token.updated_at = datetime.now()
db.commit()
logger.info(f"Auto-refreshed {token.provider}/{token.service} for company {token.company_id}")
return True
return False
except Exception as e:
logger.error(f"Token auto-refresh error {token.provider}/{token.service}: {e}")
db.rollback()
return False
def save_token(self, db, company_id: int, user_id: int, provider: str, def save_token(self, db, company_id: int, user_id: int, provider: str,
service: str, token_data: Dict) -> bool: service: str, token_data: Dict) -> bool:
"""Save or update OAuth token in database. """Save or update OAuth token in database.
@ -278,19 +299,13 @@ class OAuthService:
return None return None
# Check if token is expired # Check if token is expired
if token.is_expired and token.refresh_token: if token.is_expired:
new_data = self.refresh_access_token(provider, token.refresh_token) if token.refresh_token:
if new_data: if not self._try_refresh_token(db, token):
token.access_token = new_data.get('access_token', token.access_token) token.is_active = False
expires_in = new_data.get('expires_in') db.commit()
if expires_in: return None
token.expires_at = datetime.now() + timedelta(seconds=int(expires_in))
token.updated_at = datetime.now()
db.commit()
else: else:
# Refresh failed, mark as inactive
token.is_active = False
db.commit()
return None return None
return token.access_token return token.access_token
@ -316,12 +331,20 @@ class OAuthService:
result = {} result = {}
for token in tokens: for token in tokens:
key = f"{token.provider}/{token.service}" key = f"{token.provider}/{token.service}"
# Auto-refresh expired tokens
refresh_failed = False
if token.is_expired and token.refresh_token:
if not self._try_refresh_token(db, token):
refresh_failed = True
result[key] = { result[key] = {
'connected': True, 'connected': True,
'account_name': token.account_name, 'account_name': token.account_name,
'account_id': token.account_id, 'account_id': token.account_id,
'expires_at': token.expires_at.isoformat() if token.expires_at else None, 'expires_at': token.expires_at.isoformat() if token.expires_at else None,
'is_expired': token.is_expired, 'is_expired': token.is_expired,
'refresh_failed': refresh_failed,
'updated_at': token.updated_at.isoformat() if token.updated_at else None, 'updated_at': token.updated_at.isoformat() if token.updated_at else None,
} }
return result return result

View File

@ -366,7 +366,11 @@
{% if gbp_conn.get('is_expired') %} {% if gbp_conn.get('is_expired') %}
<div class="oauth-status expired"> <div class="oauth-status expired">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{% if gbp_conn.get('refresh_failed') %}
Automatyczne odświeżenie nie powiodło się — połącz ponownie
{% else %}
Token wygasł — wymagane ponowne połączenie Token wygasł — wymagane ponowne połączenie
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="oauth-status connected"> <div class="oauth-status connected">
@ -417,7 +421,11 @@
{% if sc_conn.get('is_expired') %} {% if sc_conn.get('is_expired') %}
<div class="oauth-status expired"> <div class="oauth-status expired">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{% if sc_conn.get('refresh_failed') %}
Automatyczne odświeżenie nie powiodło się — połącz ponownie
{% else %}
Token wygasł — wymagane ponowne połączenie Token wygasł — wymagane ponowne połączenie
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="oauth-status connected"> <div class="oauth-status connected">
@ -469,7 +477,11 @@
{% if fb_conn.get('is_expired') %} {% if fb_conn.get('is_expired') %}
<div class="oauth-status expired"> <div class="oauth-status expired">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{% if fb_conn.get('refresh_failed') %}
Automatyczne odświeżenie nie powiodło się — połącz ponownie
{% else %}
Token wygasł — wymagane ponowne połączenie Token wygasł — wymagane ponowne połączenie
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="oauth-status connected"> <div class="oauth-status connected">
@ -516,7 +528,11 @@
{% if ig_conn.get('is_expired') %} {% if ig_conn.get('is_expired') %}
<div class="oauth-status expired"> <div class="oauth-status expired">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
{% if ig_conn.get('refresh_failed') %}
Automatyczne odświeżenie nie powiodło się — połącz ponownie
{% else %}
Token wygasł — wymagane ponowne połączenie Token wygasł — wymagane ponowne połączenie
{% endif %}
</div> </div>
{% else %} {% else %}
<div class="oauth-status connected"> <div class="oauth-status connected">