feat: add manual token refresh capability via UI and backend API endpoint

This commit is contained in:
ramvignesh-b
2026-05-11 15:54:05 +05:30
parent 6a34163bf4
commit 7d0a8f3dd8
4 changed files with 77 additions and 20 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ export class TokenManager {
return tokens.accessToken;
}
async forceRefresh(providerName: string): Promise<string | null> {
async refreshAccessToken(providerName: string): Promise<string | null> {
const refreshKey = `provider:${providerName}:refresh_token`;
const refreshToken = await this.redis.get(refreshKey);
if (!refreshToken) return null;
+6 -2
View File
@@ -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,
};
}
+27 -11
View File
@@ -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 };
+43 -6
View File
@@ -389,12 +389,19 @@
<span class="text-xs font-medium opacity-60">${formatTimeAgo(status.lastUpdated)}</span>
</div>
<div class="grid grid-cols-2 gap-2">
<a href="/auth/${name}/login" target="_blank" class="btn btn-primary btn-sm flex-1 shadow-sm">
<i class="ph-bold ph-link"></i> Connect
</a>
<button type="button" onclick="window.editProvider('${name}')" class="btn btn-outline btn-sm flex-1">
<i class="ph-bold ph-pencil-simple"></i> Edit
<div class="flex flex-col gap-2">
<div class="grid grid-cols-2 gap-2">
<a href="/auth/${name}/login" target="_blank" class="btn btn-primary btn-sm shadow-sm">
<i class="ph-bold ph-link"></i> Connect
</a>
<button type="button" onclick="window.editProvider('${name}')" class="btn btn-outline btn-sm">
<i class="ph-bold ph-pencil-simple"></i> Edit
</button>
</div>
<button type="button" onclick="window.forceRefresh('${name}')" id="btn-refresh-${name}"
class="btn btn-ghost btn-sm border border-base-300 w-full">
<i class="ph-duotone ph-arrows-clockwise text-base mr-1" id="icon-refresh-${name}"></i>
<span class="text-[10px] uppercase font-bold tracking-wider">Manual Refresh</span>
</button>
</div>
</div>
@@ -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();