Added profile ARN handling in OAuth provider mapping and improved polling logic in OAuth modal for better user experience.

This commit is contained in:
decolua
2026-03-06 00:21:27 +07:00
parent f4e08fcd16
commit 75f486b7a2
5 changed files with 105 additions and 17 deletions
+67 -7
View File
@@ -470,13 +470,12 @@ async function getCodexUsage(accessToken) {
* Kiro (AWS CodeWhisperer) Usage * Kiro (AWS CodeWhisperer) Usage
*/ */
async function getKiroUsage(accessToken, providerSpecificData) { async function getKiroUsage(accessToken, providerSpecificData) {
try { // Default profileArn fallback
const profileArn = providerSpecificData?.profileArn; const DEFAULT_PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
if (!profileArn) { const profileArn = providerSpecificData?.profileArn || DEFAULT_PROFILE_ARN;
return { message: "Kiro connected. Profile ARN not available for quota tracking." };
}
// Kiro uses AWS CodeWhisperer GetUsageLimits API try {
// Try old API first (POST method)
const payload = { const payload = {
origin: "AI_EDITOR", origin: "AI_EDITOR",
profileArn: profileArn, profileArn: profileArn,
@@ -550,7 +549,68 @@ async function getKiroUsage(accessToken, providerSpecificData) {
quotas: quotaInfo, quotas: quotaInfo,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to fetch Kiro usage: ${error.message}`); // Fallback to new API (GET method)
try {
const params = new URLSearchParams({
origin: "AI_EDITOR",
profileArn: profileArn,
resourceType: "AGENTIC_REQUEST",
});
const fallbackResponse = await fetch(`https://q.us-east-1.amazonaws.com/getUsageLimits?${params}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
},
});
if (!fallbackResponse.ok) {
throw new Error(`Fallback API error (${fallbackResponse.status})`);
}
const fallbackData = await fallbackResponse.json();
// Parse new API response structure
const usageList = fallbackData.usageBreakdownList || [];
const quotaInfo = {};
const resetAt = parseResetTime(fallbackData.nextDateReset || fallbackData.resetDate);
usageList.forEach((breakdown) => {
const resourceType = breakdown.resourceType?.toLowerCase() || "unknown";
const used = breakdown.currentUsageWithPrecision || 0;
const total = breakdown.usageLimitWithPrecision || 0;
quotaInfo[resourceType] = {
used,
total,
remaining: total - used,
resetAt,
unlimited: false,
};
// Add free trial if available
if (breakdown.freeTrialInfo) {
const freeUsed = breakdown.freeTrialInfo.currentUsageWithPrecision || 0;
const freeTotal = breakdown.freeTrialInfo.usageLimitWithPrecision || 0;
quotaInfo[`${resourceType}_freetrial`] = {
used: freeUsed,
total: freeTotal,
remaining: freeTotal - freeUsed,
resetAt: parseResetTime(breakdown.freeTrialInfo.freeTrialExpiry),
unlimited: false,
};
}
});
return {
plan: fallbackData.subscriptionInfo?.subscriptionTitle || "Kiro",
quotas: quotaInfo,
};
} catch (fallbackError) {
throw new Error(`Failed to fetch Kiro usage: ${error.message} | Fallback: ${fallbackError.message}`);
}
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "9router-app", "name": "9router-app",
"version": "0.3.31", "version": "0.3.32",
"description": "9Router web dashboard", "description": "9Router web dashboard",
"private": true, "private": true,
"scripts": { "scripts": {
+14 -9
View File
@@ -660,6 +660,7 @@ const PROVIDERS = {
access_token: data.accessToken, access_token: data.accessToken,
refresh_token: data.refreshToken, refresh_token: data.refreshToken,
expires_in: data.expiresIn, expires_in: data.expiresIn,
profile_arn: data?.profileArn || null,
// Store client credentials for refresh // Store client credentials for refresh
_clientId: extraData?._clientId, _clientId: extraData?._clientId,
_clientSecret: extraData?._clientSecret, _clientSecret: extraData?._clientSecret,
@@ -675,15 +676,19 @@ const PROVIDERS = {
}, },
}; };
}, },
mapTokens: (tokens) => ({ mapTokens: (tokens) => {
accessToken: tokens.access_token, const mapped = {
refreshToken: tokens.refresh_token, accessToken: tokens.access_token,
expiresIn: tokens.expires_in, refreshToken: tokens.refresh_token,
providerSpecificData: { expiresIn: tokens.expires_in,
clientId: tokens._clientId, providerSpecificData: {
clientSecret: tokens._clientSecret, profileArn: tokens?.profile_arn || null,
}, clientId: tokens._clientId,
}), clientSecret: tokens._clientSecret,
},
};
return mapped;
},
}, },
cursor: { cursor: {
@@ -43,12 +43,14 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
setAuthMethod(null); setAuthMethod(null);
setSocialProvider(null); setSocialProvider(null);
onSuccess?.(); onSuccess?.();
onClose?.(); // Close modal after success
}; };
const handleDeviceSuccess = () => { const handleDeviceSuccess = () => {
setAuthMethod(null); setAuthMethod(null);
setIdcConfig(null); setIdcConfig(null);
onSuccess?.(); onSuccess?.();
onClose?.(); // Close modal after success
}; };
// Show method selection first // Show method selection first
+21
View File
@@ -19,6 +19,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
const [deviceData, setDeviceData] = useState(null); const [deviceData, setDeviceData] = useState(null);
const [polling, setPolling] = useState(false); const [polling, setPolling] = useState(false);
const popupRef = useRef(null); const popupRef = useRef(null);
const pollingAbortRef = useRef(false);
const { copied, copy } = useCopyToClipboard(); const { copied, copy } = useCopyToClipboard();
// State for client-only values to avoid hydration mismatch // State for client-only values to avoid hydration mismatch
@@ -66,12 +67,27 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
// Poll for device code token // Poll for device code token
const startPolling = useCallback(async (deviceCode, codeVerifier, interval, extraData) => { const startPolling = useCallback(async (deviceCode, codeVerifier, interval, extraData) => {
pollingAbortRef.current = false;
setPolling(true); setPolling(true);
const maxAttempts = 60; const maxAttempts = 60;
for (let i = 0; i < maxAttempts; i++) { for (let i = 0; i < maxAttempts; i++) {
// Check if polling should be aborted
if (pollingAbortRef.current) {
console.log("[OAuthModal] Polling aborted");
setPolling(false);
return;
}
await new Promise((r) => setTimeout(r, interval * 1000)); await new Promise((r) => setTimeout(r, interval * 1000));
// Check again after sleep
if (pollingAbortRef.current) {
console.log("[OAuthModal] Polling aborted after sleep");
setPolling(false);
return;
}
try { try {
const res = await fetch(`/api/oauth/${provider}/poll`, { const res = await fetch(`/api/oauth/${provider}/poll`, {
method: "POST", method: "POST",
@@ -82,6 +98,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
pollingAbortRef.current = true; // Stop polling immediately
setStep("success"); setStep("success");
setPolling(false); setPolling(false);
onSuccess?.(); onSuccess?.();
@@ -182,7 +199,11 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setIsDeviceCode(false); setIsDeviceCode(false);
setDeviceData(null); setDeviceData(null);
setPolling(false); setPolling(false);
pollingAbortRef.current = false;
startOAuthFlow(); startOAuthFlow();
} else if (!isOpen) {
// Abort polling when modal closes
pollingAbortRef.current = true;
} }
}, [isOpen, provider, startOAuthFlow]); }, [isOpen, provider, startOAuthFlow]);