mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
* feat(api-keys): add rename support in permissions modal Add an editable key name field at the top of the permissions modal, allowing users to rename API keys alongside existing permission settings. The backend already supported name updates via PATCH /api/keys/:id — this wires the UI to send the name field and refreshes the key list on success. Changes: - Add keyName state and text input to PermissionsModal - Update handleUpdatePermissions to validate and send name in PATCH body - Add integration test for rename via PATCH (valid, empty, too-long names) - Update E2E mock to handle PATCH requests * chore(release): bump version to 3.7.6 * chore(release): v3.7.6 — merge API key rename feature and sync docs * chore(release): expand contributor credits to 155 PRs across full project history - Expanded acknowledgment table from 29 to 53 contributors - Added 100+ previously uncredited PRs from project inception through v3.7.5 - Moved contributor credits section to v3.7.6 (current release) - Synced llm.txt version to 3.7.6 * fix: resolve security ReDoS in codex and bugs #1797 #1789 * feat(dashboard): implement remaining v3.7.6 dashboard features and fixes * fix(xiaomi-mimo): update models to V2.5, fix Token Plan validation and default region (#1823) Integrated into release/v3.7.6 * fix(dashboard): correct loadPresets ReferenceError in CostOverviewTab * fix(codex): omit compact client metadata (#1822) Integrated into release/v3.7.6 * feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821) Integrated into release/v3.7.6 * Fix endpoint visibility, A2A status, and API catalog (#1806) Integrated into release/v3.7.6 * fix(analytics): use pure SQL aggregations — no history rows loaded (#1802) Integrated into release/v3.7.6 * fix(stability): resolve codex input validation, enable combo circuit breaker, and fix broken unit tests * docs(changelog): update for stability bug fixes #1804 #1805 * fix: clear active requests and recover providers (#1824) Integrated into release/v3.7.6 * feat: inject fallback tool names to prevent upstream 400 errors (#1775) * feat: auto-restore probe-failed database to prevent data loss (#1810) * fix: safely cast inputs to strings before calling trim() to avoid crashes on numeric fields in proxy modal (#1825) * chore(release): v3.7.6 — final stability patches for production * test: update expected db probe-failure error message for auto-restore feature * chore(workflow): mandate implementation plan generation in resolve-issues * docs(changelog): rewrite v3.7.6 with complete commit-accurate entries * feat(analytics): add cost-based usage insights and activity streaks Expand usage analytics to report total cost, per-series cost totals, API key counts, and current activity streaks using pricing-aware token calculations. Also make probe-failed database recovery choose the newest backup by its embedded timestamp instead of filesystem mtime so auto-restore selects the intended snapshot reliably. * fix(mitm): enforce transparent interception on port 443 only Reject non-443 MITM port updates in the settings API and normalize stored configuration back to the required transparent interception port. Lock the dashboard port field to 443, update the validation copy, and add integration coverage to prevent stale custom ports from being accepted or surfaced. * docs(changelog): update for analytics and mitm features --------- Co-authored-by: Andrew Munsell <andrew@wizardapps.net> Co-authored-by: Antigravity Assistant <bot@antigravity.local> Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Co-authored-by: Sergey Morozov <tr0st@bk.ru> Co-authored-by: payne <baboialex95@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: ipanghu <bypanghu@163.com>
412 lines
15 KiB
TypeScript
412 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Modal from "./Modal";
|
|
import Button from "./Button";
|
|
import Input from "./Input";
|
|
|
|
type KiroAuthModalProps = {
|
|
isOpen: boolean;
|
|
providerId?: string;
|
|
providerLabel?: string;
|
|
onMethodSelect: (method: string, config?: Record<string, unknown>) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
/**
|
|
* Kiro Auth Method Selection Modal
|
|
* Auto-detects token from AWS SSO cache or allows manual import
|
|
*/
|
|
export default function KiroAuthModal({
|
|
isOpen,
|
|
providerId = "kiro",
|
|
providerLabel = "Kiro",
|
|
onMethodSelect,
|
|
onClose,
|
|
}: KiroAuthModalProps) {
|
|
const [selectedMethod, setSelectedMethod] = useState(null);
|
|
const [idcStartUrl, setIdcStartUrl] = useState("");
|
|
const [idcRegion, setIdcRegion] = useState("us-east-1");
|
|
const [refreshToken, setRefreshToken] = useState("");
|
|
const [error, setError] = useState(null);
|
|
const [importing, setImporting] = useState(false);
|
|
const [autoDetecting, setAutoDetecting] = useState(false);
|
|
const [autoDetected, setAutoDetected] = useState(false);
|
|
|
|
// Auto-detect token when import method is selected
|
|
useEffect(() => {
|
|
if (selectedMethod !== "import" || !isOpen) return;
|
|
|
|
const autoDetect = async () => {
|
|
setAutoDetecting(true);
|
|
setError(null);
|
|
setAutoDetected(false);
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`/api/oauth/kiro/auto-import?targetProvider=${encodeURIComponent(providerId)}`
|
|
);
|
|
const data = await res.json();
|
|
|
|
if (data.found) {
|
|
setRefreshToken(data.refreshToken);
|
|
setAutoDetected(true);
|
|
} else {
|
|
setError(data.error || "Could not auto-detect token");
|
|
}
|
|
} catch (err) {
|
|
setError("Failed to auto-detect token");
|
|
} finally {
|
|
setAutoDetecting(false);
|
|
}
|
|
};
|
|
|
|
autoDetect();
|
|
}, [providerId, selectedMethod, isOpen]);
|
|
|
|
const handleMethodSelect = (method) => {
|
|
setSelectedMethod(method);
|
|
setError(null);
|
|
};
|
|
|
|
const handleBack = () => {
|
|
setSelectedMethod(null);
|
|
setError(null);
|
|
};
|
|
|
|
const handleImportToken = async () => {
|
|
if (!refreshToken.trim()) {
|
|
setError("Please enter a refresh token");
|
|
return;
|
|
}
|
|
|
|
setImporting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`/api/oauth/kiro/import?targetProvider=${encodeURIComponent(providerId)}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ refreshToken: refreshToken.trim() }),
|
|
}
|
|
);
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || "Import failed");
|
|
}
|
|
|
|
// Success - close modal
|
|
onClose();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setImporting(false);
|
|
}
|
|
};
|
|
|
|
const handleIdcContinue = () => {
|
|
if (!idcStartUrl.trim()) {
|
|
setError("Please enter your IDC start URL");
|
|
return;
|
|
}
|
|
onMethodSelect("idc", { startUrl: idcStartUrl.trim(), region: idcRegion });
|
|
};
|
|
|
|
const handleSocialLogin = (provider) => {
|
|
onMethodSelect("social", { provider });
|
|
};
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} title={`Connect ${providerLabel}`} onClose={onClose} size="lg">
|
|
<div className="flex flex-col gap-4">
|
|
{/* Method Selection */}
|
|
{!selectedMethod && (
|
|
<div className="space-y-3">
|
|
<p className="text-sm text-text-muted mb-4">Choose your authentication method:</p>
|
|
|
|
{/* AWS Builder ID */}
|
|
<button
|
|
onClick={() => onMethodSelect("builder-id")}
|
|
className="w-full p-4 text-left border border-border rounded-lg hover:bg-sidebar transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="material-symbols-outlined text-primary mt-0.5">shield</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold mb-1">AWS Builder ID</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Recommended for most users. Sign in with the AWS account linked to{" "}
|
|
{providerLabel}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* AWS IAM Identity Center (IDC) */}
|
|
<button
|
|
onClick={() => handleMethodSelect("idc")}
|
|
className="w-full p-4 text-left border border-border rounded-lg hover:bg-sidebar transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="material-symbols-outlined text-primary mt-0.5">business</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold mb-1">
|
|
Your Organization (AWS IAM Identity Center)
|
|
</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Use your company SSO start URL (example: https://your-org.awsapps.com/start).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Google Social Login - HIDDEN */}
|
|
<button
|
|
onClick={() => handleMethodSelect("social-google")}
|
|
className="hidden w-full p-4 text-left border border-border rounded-lg hover:bg-sidebar transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="material-symbols-outlined text-primary mt-0.5">
|
|
account_circle
|
|
</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold mb-1">Google Account</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Login with your Google account (manual callback).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* GitHub Social Login - HIDDEN */}
|
|
<button
|
|
onClick={() => handleMethodSelect("social-github")}
|
|
className="hidden w-full p-4 text-left border border-border rounded-lg hover:bg-sidebar transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="material-symbols-outlined text-primary mt-0.5">code</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold mb-1">GitHub Account</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Login with your GitHub account (manual callback).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Import Token */}
|
|
<button
|
|
onClick={() => handleMethodSelect("import")}
|
|
className="w-full p-4 text-left border border-border rounded-lg hover:bg-sidebar transition-colors"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<span className="material-symbols-outlined text-primary mt-0.5">file_upload</span>
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold mb-1">Import Token</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Paste a refresh token exported from {providerLabel}.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* IDC Configuration */}
|
|
{selectedMethod === "idc" && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
IDC Start URL <span className="text-red-500">*</span>
|
|
</label>
|
|
<Input
|
|
value={idcStartUrl}
|
|
onChange={(e) => setIdcStartUrl(e.target.value)}
|
|
placeholder="https://your-org.awsapps.com/start"
|
|
className="font-mono text-sm"
|
|
/>
|
|
<p className="text-xs text-text-muted mt-1">
|
|
Your organization's AWS IAM Identity Center URL
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">AWS Region</label>
|
|
<Input
|
|
value={idcRegion}
|
|
onChange={(e) => setIdcRegion(e.target.value)}
|
|
placeholder="us-east-1"
|
|
className="font-mono text-sm"
|
|
/>
|
|
<p className="text-xs text-text-muted mt-1">
|
|
AWS region for your Identity Center (default: us-east-1)
|
|
</p>
|
|
</div>
|
|
|
|
{error && <p className="text-sm text-red-600">{error}</p>}
|
|
|
|
<div className="flex gap-2">
|
|
<Button onClick={handleIdcContinue} fullWidth>
|
|
Continue
|
|
</Button>
|
|
<Button onClick={handleBack} variant="ghost" fullWidth>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Social Login Info (Google) */}
|
|
{selectedMethod === "social-google" && (
|
|
<div className="space-y-4">
|
|
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg border border-amber-200 dark:border-amber-800">
|
|
<div className="flex gap-2">
|
|
<span className="material-symbols-outlined text-amber-600 dark:text-amber-400">
|
|
info
|
|
</span>
|
|
<div className="flex-1 text-sm">
|
|
<p className="font-medium text-amber-900 dark:text-amber-100 mb-1">
|
|
Manual Callback Required
|
|
</p>
|
|
<p className="text-amber-800 dark:text-amber-200">
|
|
After login, you'll need to copy the callback URL from your browser and
|
|
paste it back here.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button onClick={() => handleSocialLogin("google")} fullWidth>
|
|
Continue with Google
|
|
</Button>
|
|
<Button onClick={handleBack} variant="ghost" fullWidth>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Social Login Info (GitHub) */}
|
|
{selectedMethod === "social-github" && (
|
|
<div className="space-y-4">
|
|
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg border border-amber-200 dark:border-amber-800">
|
|
<div className="flex gap-2">
|
|
<span className="material-symbols-outlined text-amber-600 dark:text-amber-400">
|
|
info
|
|
</span>
|
|
<div className="flex-1 text-sm">
|
|
<p className="font-medium text-amber-900 dark:text-amber-100 mb-1">
|
|
Manual Callback Required
|
|
</p>
|
|
<p className="text-amber-800 dark:text-amber-200">
|
|
After login, you'll need to copy the callback URL from your browser and
|
|
paste it back here.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<Button onClick={() => handleSocialLogin("github")} fullWidth>
|
|
Continue with GitHub
|
|
</Button>
|
|
<Button onClick={handleBack} variant="ghost" fullWidth>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Import Token */}
|
|
{selectedMethod === "import" && (
|
|
<div className="space-y-4">
|
|
{/* Auto-detecting state */}
|
|
{autoDetecting && (
|
|
<div className="text-center py-6">
|
|
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
|
|
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
|
|
progress_activity
|
|
</span>
|
|
</div>
|
|
<h3 className="text-lg font-semibold mb-2">Auto-detecting token...</h3>
|
|
<p className="text-sm text-text-muted">
|
|
Reading {providerLabel} credentials from AWS SSO cache
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Form (shown after auto-detect completes) */}
|
|
{!autoDetecting && (
|
|
<>
|
|
{/* Success message if auto-detected */}
|
|
{autoDetected && (
|
|
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div className="flex gap-2">
|
|
<span className="material-symbols-outlined text-green-600 dark:text-green-400">
|
|
check_circle
|
|
</span>
|
|
<p className="text-sm text-green-800 dark:text-green-200">
|
|
Token auto-detected from {providerLabel} successfully!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Info message if not auto-detected */}
|
|
{!autoDetected && !error && (
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<div className="flex gap-2">
|
|
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400">
|
|
info
|
|
</span>
|
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
{providerLabel} token was not auto-detected. Please paste your refresh token
|
|
manually.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2">
|
|
Refresh Token <span className="text-red-500">*</span>
|
|
</label>
|
|
<Input
|
|
value={refreshToken}
|
|
onChange={(e) => setRefreshToken(e.target.value)}
|
|
placeholder="Token will be auto-filled..."
|
|
className="font-mono text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">
|
|
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={handleImportToken}
|
|
fullWidth
|
|
disabled={importing || !refreshToken.trim()}
|
|
>
|
|
{importing ? "Importing..." : "Import Token"}
|
|
</Button>
|
|
<Button onClick={handleBack} variant="ghost" fullWidth>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|