mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Added profile ARN handling in OAuth provider mapping and improved polling logic in OAuth modal for better user experience.
This commit is contained in:
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user