diff --git a/src/core/TokenManager.ts b/src/core/TokenManager.ts index a5c30fb..083b832 100644 --- a/src/core/TokenManager.ts +++ b/src/core/TokenManager.ts @@ -21,7 +21,7 @@ export class TokenManager { return tokens.accessToken; } - async forceRefresh(providerName: string): Promise { + async refreshAccessToken(providerName: string): Promise { const refreshKey = `provider:${providerName}:refresh_token`; const refreshToken = await this.redis.get(refreshKey); if (!refreshToken) return null; diff --git a/src/providers/GenericProvider.ts b/src/providers/GenericProvider.ts index 3f3dfdc..e2086c3 100644 --- a/src/providers/GenericProvider.ts +++ b/src/providers/GenericProvider.ts @@ -4,7 +4,7 @@ import type { OAuthProvider, TokenResponse } from "./interface"; const TokenResponseSchema = z.object({ access_token: z.string(), - refresh_token: z.string(), + refresh_token: z.string().optional(), expires_in: z.number(), }); @@ -49,6 +49,10 @@ export class GenericProvider implements OAuthProvider { const rawData = await response.json(); const data = TokenResponseSchema.parse(rawData); + if (!data.refresh_token) { + throw new Error("Provider did not return a refresh token during initial exchange."); + } + return { accessToken: data.access_token, refreshToken: data.refresh_token, @@ -81,7 +85,7 @@ export class GenericProvider implements OAuthProvider { return { accessToken: data.access_token, - refreshToken: data.refresh_token, + refreshToken: data.refresh_token || refreshToken, // Fallback to current token if provider doesn't rotate it expiresIn: data.expires_in, }; } diff --git a/src/routes/api.ts b/src/routes/api.ts index dc3991a..698f91b 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -36,18 +36,34 @@ apiRoutes.get("/token/:provider", async (c) => { const provider = new GenericProvider(providerName, providerConfig); const tokenManager = new TokenManager(redis, provider); - try { - const accessToken = await tokenManager.getAccessToken(providerName); - if (!accessToken) { - return c.json({ error: "No tokens found for provider" }, 404); - } - return c.json({ access_token: accessToken }); - } catch (error) { - if (error instanceof Error) { - return c.json({ error: error.message }, 500); - } - return c.json({ error: "Internal Server Error" }, 500); + const accessToken = await tokenManager.getAccessToken(providerName); + if (!accessToken) { + return c.json({ error: "No tokens found for provider" }, 404); } + return c.json({ access_token: accessToken }); +}); + +apiRoutes.post("/refresh/:provider", async (c) => { + const providerName = c.req.param("provider"); + const configManager = new ConfigManager(redis); + const providerConfig = await configManager.getProviderConfig(providerName); + + if (!providerConfig) { + return c.json({ error: `Provider ${providerName} not configured` }, 404); + } + + const provider = new GenericProvider(providerName, providerConfig); + const tokenManager = new TokenManager(redis, provider); + + await tokenManager.refreshAccessToken(providerName); + const accessToken = await redis.get(`provider:${providerName}:access_token`); + const refreshToken = await redis.get(`provider:${providerName}:refresh_token`); + const lastUpdated = await redis.get(`provider:${providerName}:last_updated`); + + return c.json({ + success: true, + status: { accessToken, refreshToken, lastUpdated }, + }); }); export { apiRoutes }; diff --git a/src/views/dashboard.html b/src/views/dashboard.html index f31dbf0..c4f7c02 100644 --- a/src/views/dashboard.html +++ b/src/views/dashboard.html @@ -389,12 +389,19 @@ ${formatTimeAgo(status.lastUpdated)} -
- - Connect - - +
+ @@ -460,6 +467,36 @@ document.getElementById('providerName').focus(); }; + window.forceRefresh = async (name) => { + const apiKey = apiKeyInput.value.trim(); + const btn = document.getElementById(`btn-refresh-${name}`); + const icon = document.getElementById(`icon-refresh-${name}`); + + if (btn) btn.classList.add('btn-disabled', 'opacity-50'); + if (icon) icon.classList.add('animate-spin'); + + try { + const response = await fetch(`/api/refresh/${name}`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${apiKey}` } + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.error || 'Refresh failed'); + + showNotification(`${name} tokens refreshed successfully!`); + // Update local data and re-render + tokenStatus[name] = data.status; + renderRegistry(); + } catch (error) { + console.error('Refresh error:', error); + showNotification(error.message, 'error'); + } finally { + if (btn) btn.classList.remove('btn-disabled', 'opacity-50'); + if (icon) icon.classList.remove('animate-spin'); + } + }; + configForm.addEventListener('submit', async (e) => { e.preventDefault(); const apiKey = apiKeyInput.value.trim();