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:
4
src/assets/icons/kiro.svg
Normal file
4
src/assets/icons/kiro.svg
Normal 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 |
@@ -675,7 +675,26 @@
|
|||||||
"iflow_cookie_result_expired": "Expires At",
|
"iflow_cookie_result_expired": "Expires At",
|
||||||
"iflow_cookie_result_path": "Saved Path",
|
"iflow_cookie_result_path": "Saved Path",
|
||||||
"iflow_cookie_result_type": "Type",
|
"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": {
|
"usage_stats": {
|
||||||
"title": "Usage Statistics",
|
"title": "Usage Statistics",
|
||||||
|
|||||||
@@ -675,7 +675,26 @@
|
|||||||
"iflow_cookie_result_expired": "过期时间",
|
"iflow_cookie_result_expired": "过期时间",
|
||||||
"iflow_cookie_result_path": "保存路径",
|
"iflow_cookie_result_path": "保存路径",
|
||||||
"iflow_cookie_result_type": "类型",
|
"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": {
|
"usage_stats": {
|
||||||
"title": "使用统计",
|
"title": "使用统计",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import iconGemini from '@/assets/icons/gemini.svg';
|
|||||||
import iconQwen from '@/assets/icons/qwen.svg';
|
import iconQwen from '@/assets/icons/qwen.svg';
|
||||||
import iconIflow from '@/assets/icons/iflow.svg';
|
import iconIflow from '@/assets/icons/iflow.svg';
|
||||||
import iconVertex from '@/assets/icons/vertex.svg';
|
import iconVertex from '@/assets/icons/vertex.svg';
|
||||||
|
import iconKiro from '@/assets/icons/kiro.svg';
|
||||||
|
|
||||||
interface ProviderState {
|
interface ProviderState {
|
||||||
url?: string;
|
url?: string;
|
||||||
@@ -54,6 +55,19 @@ interface VertexImportState {
|
|||||||
result?: VertexImportResult;
|
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 } }[] = [
|
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: '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 },
|
{ 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: '',
|
location: '',
|
||||||
loading: false
|
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 timers = useRef<Record<string, number>>({});
|
||||||
const vertexFileInputRef = useRef<HTMLInputElement | null>(null);
|
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 += `®ion=${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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h1 className={styles.pageTitle}>{t('nav.oauth', { defaultValue: 'OAuth' })}</h1>
|
<h1 className={styles.pageTitle}>{t('nav.oauth', { defaultValue: 'OAuth' })}</h1>
|
||||||
@@ -574,6 +639,95 @@ export function OAuthPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user