feat: Claude CLI OAuth token expiry warning
New GET /auth/status endpoint reads ~/.claude/.credentials.json and returns hours remaining + warning flag. UI shows a dismissible amber banner when < 24h remain, turning red if expired. Checked on page load and every 30 minutes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import uvicorn
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s: %(message)s")
|
||||
|
||||
from config import settings
|
||||
from routers import chat, google_chat, nextcloud_talk, files, distill
|
||||
from routers import chat, google_chat, nextcloud_talk, files, distill, auth
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -28,6 +28,7 @@ app.include_router(google_chat.router)
|
||||
app.include_router(nextcloud_talk.router)
|
||||
app.include_router(files.router)
|
||||
app.include_router(distill.router)
|
||||
app.include_router(auth.router)
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
|
||||
|
||||
37
cortex/routers/auth.py
Normal file
37
cortex/routers/auth.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Claude CLI OAuth token status.
|
||||
|
||||
GET /auth/status — returns expiry info; warns when < WARN_HOURS remain
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from fastapi import APIRouter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/auth")
|
||||
|
||||
CREDENTIALS_PATH = Path.home() / ".claude" / ".credentials.json"
|
||||
WARN_HOURS = 24 # show warning banner when fewer than this many hours remain
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def auth_status() -> dict:
|
||||
try:
|
||||
data = json.loads(CREDENTIALS_PATH.read_text())
|
||||
oauth = data["claudeAiOauth"]
|
||||
expires_at_ms = oauth["expiresAt"]
|
||||
expires_dt = datetime.fromtimestamp(expires_at_ms / 1000, tz=timezone.utc)
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
hours_remaining = (expires_dt - now).total_seconds() / 3600
|
||||
return {
|
||||
"ok": True,
|
||||
"expires_at": expires_dt.isoformat(),
|
||||
"hours_remaining": round(hours_remaining, 1),
|
||||
"warning": hours_remaining < WARN_HOURS,
|
||||
"expired": hours_remaining <= 0,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning("auth status check failed: %s", e)
|
||||
return {"ok": False, "error": str(e), "warning": True, "expired": False}
|
||||
@@ -905,3 +905,30 @@
|
||||
helpModal.addEventListener('click', (e) => {
|
||||
if (e.target === helpModal) helpModal.classList.remove('open');
|
||||
});
|
||||
|
||||
// ── Auth token warning banner ─────────────────────────────
|
||||
const authBanner = document.getElementById('auth-banner');
|
||||
const authBannerMsg = document.getElementById('auth-banner-msg');
|
||||
const authBannerClose = document.getElementById('auth-banner-close');
|
||||
|
||||
async function checkAuthStatus() {
|
||||
try {
|
||||
const res = await fetch('/auth/status');
|
||||
if (!res.ok) return;
|
||||
const d = await res.json();
|
||||
if (!d.warning) return;
|
||||
|
||||
const msg = d.expired
|
||||
? '✕ Claude CLI token has expired — run `claude` in a terminal to re-authenticate'
|
||||
: `⚠ Claude CLI token expires in ${d.hours_remaining}h — run \`claude\` in a terminal to re-authenticate`;
|
||||
authBannerMsg.textContent = msg;
|
||||
authBanner.classList.toggle('expired', !!d.expired);
|
||||
authBanner.classList.add('show');
|
||||
} catch { /* silently ignore — don't break the UI */ }
|
||||
}
|
||||
|
||||
authBannerClose.addEventListener('click', () => authBanner.classList.remove('show'));
|
||||
|
||||
checkAuthStatus();
|
||||
// Re-check every 30 minutes
|
||||
setInterval(checkAuthStatus, 30 * 60 * 1000);
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth warning banner — shown when Claude CLI token is near expiry -->
|
||||
<div id="auth-banner">
|
||||
<span id="auth-banner-msg"></span>
|
||||
<button id="auth-banner-close" title="Dismiss">✕</button>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
<div id="session-id"></div>
|
||||
|
||||
|
||||
@@ -898,6 +898,41 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Auth warning banner ─────────────────────────────────── */
|
||||
#auth-banner {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 20px;
|
||||
background: rgba(160, 100, 0, 0.18);
|
||||
border-bottom: 1px solid rgba(200, 140, 20, 0.45);
|
||||
font-size: 0.82rem;
|
||||
color: #c9a84c;
|
||||
}
|
||||
|
||||
#auth-banner.show { display: flex; }
|
||||
#auth-banner.expired {
|
||||
background: rgba(120, 20, 20, 0.25);
|
||||
border-color: rgba(200, 60, 60, 0.45);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
#auth-banner-msg { flex: 1; }
|
||||
|
||||
#auth-banner-close {
|
||||
background: none;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 7px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#auth-banner-close:hover { opacity: 1; }
|
||||
|
||||
/* ── Mobile responsive ───────────────────────────────────── */
|
||||
@media (max-width: 520px) {
|
||||
header { padding: 8px 12px; gap: 8px; }
|
||||
|
||||
Reference in New Issue
Block a user