feat: add manual token refresh capability via UI and backend API endpoint
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user