feat: add Kiro OAuth login support

- Add Kiro icon (kiro.svg)
- Add Kiro OAuth with AWS Builder ID and Identity Center (IDC) support
- Add Kiro refreshToken import functionality
- Add i18n translations for Kiro OAuth (en/zh-CN)

Merged from PR #13 with upstream ProviderNav changes preserved
This commit is contained in:
kongkongyo
2026-02-03 12:12:03 +08:00
4 changed files with 198 additions and 2 deletions

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="#FF9900"/>
<path d="M12 6L8 10h3v4H8l4 4 4-4h-3v-4h3l-4-4z" fill="#232F3E"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -675,7 +675,26 @@
"iflow_cookie_result_expired": "Expires At",
"iflow_cookie_result_path": "Saved Path",
"iflow_cookie_result_type": "Type",
"remote_access_disabled": "This login method is not available for remote access. Please access from localhost."
"remote_access_disabled": "This login method is not available for remote access. Please access from localhost.",
"kiro_oauth_title": "Kiro OAuth",
"kiro_oauth_hint": "Login to Kiro service via AWS SSO, supporting Builder ID and Identity Center (IDC), or import refreshToken from Kiro IDE directly.",
"kiro_builder_id_label": "AWS Builder ID Login",
"kiro_builder_id_hint": "Login with AWS Builder ID account, suitable for individual developers.",
"kiro_builder_id_button": "Login with Builder ID",
"kiro_idc_label": "AWS Identity Center (IDC) Login",
"kiro_idc_hint": "Login with enterprise AWS Identity Center, requires Start URL and Region.",
"kiro_idc_start_url_label": "Start URL",
"kiro_idc_start_url_placeholder": "https://your-org.awsapps.com/start",
"kiro_idc_region_label": "Region (Optional)",
"kiro_idc_region_placeholder": "us-east-1",
"kiro_idc_button": "Login with IDC",
"kiro_token_import_label": "Token Import",
"kiro_token_import_hint": "Import refreshToken from Kiro IDE, can be found in Kiro IDE's auth file.",
"kiro_token_placeholder": "Paste refreshToken",
"kiro_token_import_button": "Import Token",
"kiro_token_required": "Please enter refreshToken first",
"kiro_token_import_success": "Kiro Token imported successfully",
"kiro_token_import_error": "Kiro Token import failed:"
},
"usage_stats": {
"title": "Usage Statistics",

View File

@@ -675,7 +675,26 @@
"iflow_cookie_result_expired": "过期时间",
"iflow_cookie_result_path": "保存路径",
"iflow_cookie_result_type": "类型",
"remote_access_disabled": "远程访问不支持此登录方式,请从本地 (localhost) 访问"
"remote_access_disabled": "远程访问不支持此登录方式,请从本地 (localhost) 访问",
"kiro_oauth_title": "Kiro OAuth",
"kiro_oauth_hint": "通过 AWS SSO 登录 Kiro 服务,支持 Builder ID 和 Identity Center (IDC) 两种方式,或直接导入 Kiro IDE 的 refreshToken。",
"kiro_builder_id_label": "AWS Builder ID 登录",
"kiro_builder_id_hint": "使用 AWS Builder ID 账号登录,适用于个人开发者。",
"kiro_builder_id_button": "使用 Builder ID 登录",
"kiro_idc_label": "AWS Identity Center (IDC) 登录",
"kiro_idc_hint": "使用企业 AWS Identity Center 登录,需要提供 Start URL 和 Region。",
"kiro_idc_start_url_label": "Start URL",
"kiro_idc_start_url_placeholder": "https://your-org.awsapps.com/start",
"kiro_idc_region_label": "Region (可选)",
"kiro_idc_region_placeholder": "us-east-1",
"kiro_idc_button": "使用 IDC 登录",
"kiro_token_import_label": "Token 导入",
"kiro_token_import_hint": "从 Kiro IDE 导入 refreshToken可在 Kiro IDE 的认证文件中找到。",
"kiro_token_placeholder": "粘贴 refreshToken",
"kiro_token_import_button": "导入 Token",
"kiro_token_required": "请先填写 refreshToken",
"kiro_token_import_success": "Kiro Token 导入成功",
"kiro_token_import_error": "Kiro Token 导入失败:"
},
"usage_stats": {
"title": "使用统计",

View File

@@ -15,6 +15,7 @@ import iconGemini from '@/assets/icons/gemini.svg';
import iconQwen from '@/assets/icons/qwen.svg';
import iconIflow from '@/assets/icons/iflow.svg';
import iconVertex from '@/assets/icons/vertex.svg';
import iconKiro from '@/assets/icons/kiro.svg';
interface ProviderState {
url?: string;
@@ -54,6 +55,19 @@ interface VertexImportState {
result?: VertexImportResult;
}
interface KiroOAuthState {
method: 'builder-id' | 'idc' | null;
startUrl: string;
region: string;
}
interface KiroTokenImportState {
token: string;
loading: boolean;
error?: string;
success?: boolean;
}
const PROVIDERS: { id: OAuthProvider; titleKey: string; hintKey: string; urlLabelKey: string; icon: string | { light: string; dark: string } }[] = [
{ id: 'codex', titleKey: 'auth_login.codex_oauth_title', hintKey: 'auth_login.codex_oauth_hint', urlLabelKey: 'auth_login.codex_oauth_url_label', icon: { light: iconCodexLight, dark: iconCodexDark } },
{ id: 'anthropic', titleKey: 'auth_login.anthropic_oauth_title', hintKey: 'auth_login.anthropic_oauth_hint', urlLabelKey: 'auth_login.anthropic_oauth_url_label', icon: iconClaude },
@@ -82,6 +96,15 @@ export function OAuthPage() {
location: '',
loading: false
});
const [kiroOAuth, setKiroOAuth] = useState<KiroOAuthState>({
method: null,
startUrl: '',
region: ''
});
const [kiroTokenImport, setKiroTokenImport] = useState<KiroTokenImportState>({
token: '',
loading: false
});
const timers = useRef<Record<string, number>>({});
const vertexFileInputRef = useRef<HTMLInputElement | null>(null);
@@ -303,6 +326,48 @@ export function OAuthPage() {
}
};
const openKiroOAuth = (method: 'builder-id' | 'idc') => {
const baseUrl = window.location.origin;
let url = `${baseUrl}/v0/oauth/kiro/start?method=${method}`;
if (method === 'idc') {
const startUrl = kiroOAuth.startUrl.trim();
const region = kiroOAuth.region.trim();
if (startUrl) {
url += `&start_url=${encodeURIComponent(startUrl)}`;
}
if (region) {
url += `&region=${encodeURIComponent(region)}`;
}
}
window.open(url, '_blank', 'noopener,noreferrer');
};
const handleKiroTokenImport = async () => {
const token = kiroTokenImport.token.trim();
if (!token) {
showNotification(t('auth_login.kiro_token_required'), 'warning');
return;
}
setKiroTokenImport((prev) => ({ ...prev, loading: true, error: undefined, success: undefined }));
try {
const baseUrl = window.location.origin;
const response = await fetch(`${baseUrl}/v0/oauth/kiro/import`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
setKiroTokenImport((prev) => ({ ...prev, loading: false, success: true }));
showNotification(t('auth_login.kiro_token_import_success'), 'success');
} catch (err: any) {
setKiroTokenImport((prev) => ({ ...prev, loading: false, error: err?.message }));
showNotification(`${t('auth_login.kiro_token_import_error')} ${err?.message || ''}`, 'error');
}
};
return (
<div className={styles.container}>
<h1 className={styles.pageTitle}>{t('nav.oauth', { defaultValue: 'OAuth' })}</h1>
@@ -574,6 +639,95 @@ export function OAuthPage() {
</div>
)}
</Card>
{/* Kiro OAuth 登录 */}
<Card
title={
<span className={styles.cardTitle}>
<img src={iconKiro} alt="" className={styles.cardTitleIcon} />
{t('auth_login.kiro_oauth_title')}
</span>
}
>
<div className="hint">{t('auth_login.kiro_oauth_hint')}</div>
{/* AWS Builder ID 登录 */}
<div className="form-group" style={{ marginTop: 16 }}>
<label className="label">{t('auth_login.kiro_builder_id_label')}</label>
<div className="hint">{t('auth_login.kiro_builder_id_hint')}</div>
<Button
variant="secondary"
size="sm"
onClick={() => openKiroOAuth('builder-id')}
style={{ marginTop: 8 }}
>
{t('auth_login.kiro_builder_id_button')}
</Button>
</div>
{/* AWS Identity Center (IDC) 登录 */}
<div className="form-group" style={{ marginTop: 16 }}>
<label className="label">{t('auth_login.kiro_idc_label')}</label>
<div className="hint">{t('auth_login.kiro_idc_hint')}</div>
<Input
label={t('auth_login.kiro_idc_start_url_label')}
value={kiroOAuth.startUrl}
onChange={(e) => setKiroOAuth((prev) => ({ ...prev, startUrl: e.target.value }))}
placeholder={t('auth_login.kiro_idc_start_url_placeholder')}
/>
<Input
label={t('auth_login.kiro_idc_region_label')}
value={kiroOAuth.region}
onChange={(e) => setKiroOAuth((prev) => ({ ...prev, region: e.target.value }))}
placeholder={t('auth_login.kiro_idc_region_placeholder')}
/>
<Button
variant="secondary"
size="sm"
onClick={() => openKiroOAuth('idc')}
style={{ marginTop: 8 }}
>
{t('auth_login.kiro_idc_button')}
</Button>
</div>
{/* Token 导入 */}
<div className="form-group" style={{ marginTop: 16 }}>
<label className="label">{t('auth_login.kiro_token_import_label')}</label>
<div className="hint">{t('auth_login.kiro_token_import_hint')}</div>
<Input
value={kiroTokenImport.token}
onChange={(e) =>
setKiroTokenImport((prev) => ({
...prev,
token: e.target.value,
error: undefined,
success: undefined
}))
}
placeholder={t('auth_login.kiro_token_placeholder')}
/>
<Button
variant="secondary"
size="sm"
onClick={handleKiroTokenImport}
loading={kiroTokenImport.loading}
style={{ marginTop: 8 }}
>
{t('auth_login.kiro_token_import_button')}
</Button>
{kiroTokenImport.success && (
<div className="status-badge success" style={{ marginTop: 8 }}>
{t('auth_login.kiro_token_import_success')}
</div>
)}
{kiroTokenImport.error && (
<div className="status-badge error" style={{ marginTop: 8 }}>
{t('auth_login.kiro_token_import_error')} {kiroTokenImport.error}
</div>
)}
</div>
</Card>
</div>
</div>
);