feat: add API key validation flow for AI provider config
All checks were successful
Beta Release / beta (push) Successful in 37s
All checks were successful
Beta Release / beta (push) Successful in 37s
- Add POST /api/providers/validate backend endpoint that sends a test request to the provider's chat/completions API to verify the key - Add validateProvider to frontend API client - Redesign PanelProviders: show token input inline with Validate button, display valid/invalid badge after validation, Save only appears after successful validation - Add i18n keys (EN/FR) for validation flow 💾 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
This commit is contained in:
@@ -252,48 +252,79 @@ function PanelProfile({ config, editProfile, profileForm, setProfileForm, setEdi
|
||||
}
|
||||
|
||||
function PanelProviders({ providers, editProvider, providerForm, setProviderForm, setEditProvider, openProviderEdit, handleSaveProvider, api, loadData, t }) {
|
||||
const [validating, setValidating] = useState(null)
|
||||
const [validationStatus, setValidationStatus] = useState(null)
|
||||
|
||||
const handleValidate = async (name, apiKey, model, baseUrl) => {
|
||||
setValidating(name)
|
||||
setValidationStatus(null)
|
||||
try {
|
||||
await api.validateProvider({ name, api_key: apiKey, model, base_url: baseUrl })
|
||||
setValidationStatus({ provider: name, valid: true })
|
||||
} catch (err) {
|
||||
const msg = err.message || ''
|
||||
if (msg.includes('invalid_api_key')) {
|
||||
setValidationStatus({ provider: name, valid: false, error: t('config.keyInvalid') })
|
||||
} else {
|
||||
setValidationStatus({ provider: name, valid: false, error: `${t('config.connectionFailed')}: ${msg}` })
|
||||
}
|
||||
}
|
||||
setValidating(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-providers-list">
|
||||
{providers.map((p, i) => (
|
||||
<div key={i} className="config-card provider-card-v2">
|
||||
<div className="provider-card-top">
|
||||
<div className="provider-card-identity">
|
||||
<span className="provider-card-name">{p.name}</span>
|
||||
{p.active && <span className="badge accent">{t('config.active')}</span>}
|
||||
</div>
|
||||
<div className="provider-card-actions">
|
||||
{editProvider !== p.name && (
|
||||
<button className="ghost sm" onClick={() => openProviderEdit(p)}>{t('config.editProvider')}</button>
|
||||
)}
|
||||
{!p.active && editProvider !== p.name && (
|
||||
<button className="sm" onClick={async () => {
|
||||
await api.saveProvider({ name: p.name, active: true })
|
||||
loadData()
|
||||
}}>{t('config.activate')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editProvider !== p.name ? (
|
||||
<div className="provider-card-meta">
|
||||
<span className="mono">{p.model || '—'}</span>
|
||||
<span style={{ color: p.apiKey ? 'var(--success)' : 'var(--error)' }}>
|
||||
{p.apiKey ? t('config.keyConfigured') : t('config.noKey')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="provider-card-form">
|
||||
<FormInput label={t('config.apiKey')} value={providerForm.api_key} onChange={v => setProviderForm(f => ({ ...f, api_key: v }))} type="password" />
|
||||
<FormInput label={t('config.model')} value={providerForm.model} onChange={v => setProviderForm(f => ({ ...f, model: v }))} />
|
||||
<FormInput label={t('config.baseUrl')} value={providerForm.base_url} onChange={v => setProviderForm(f => ({ ...f, base_url: v }))} />
|
||||
<div className="config-card-actions">
|
||||
<button className="primary sm" onClick={handleSaveProvider}>{t('config.save')}</button>
|
||||
<button className="ghost sm" onClick={() => setEditProvider(null)}>{t('config.cancel')}</button>
|
||||
<div className="provider-setup-hint">{t('config.setupDescription')}</div>
|
||||
{providers.map((p, i) => {
|
||||
const isEditing = editProvider === p.name
|
||||
const isValidationTarget = validationStatus?.provider === p.name
|
||||
return (
|
||||
<div key={i} className="config-card provider-card-v2">
|
||||
<div className="provider-card-top">
|
||||
<div className="provider-card-identity">
|
||||
<span className="provider-card-name">{p.name}</span>
|
||||
{p.apiKey && <span className="badge ok">{t('config.keyConfigured')}</span>}
|
||||
{!p.apiKey && <span className="badge error">{t('config.noKey')}</span>}
|
||||
{isValidationTarget && validationStatus.valid && <span className="badge ok">{t('config.keyValid')}</span>}
|
||||
{isValidationTarget && !validationStatus.valid && <span className="badge error">{validationStatus.error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="provider-card-form">
|
||||
<div className="provider-setup-token-row">
|
||||
<div className="provider-setup-token-input">
|
||||
<label className="config-form-label">{t('config.apiKey')}</label>
|
||||
<input
|
||||
className="config-form-input"
|
||||
type="password"
|
||||
placeholder={t('config.tokenPlaceholder')}
|
||||
value={isEditing ? providerForm.api_key : ''}
|
||||
onChange={e => {
|
||||
if (!isEditing) openProviderEdit(p)
|
||||
setProviderForm(f => ({ ...f, api_key: e.target.value }))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="provider-setup-token-actions">
|
||||
<button
|
||||
className="sm primary"
|
||||
disabled={validating === p.name || !providerForm.api_key}
|
||||
onClick={() => handleValidate(p.name, providerForm.api_key, providerForm.model, providerForm.base_url)}
|
||||
>
|
||||
{validating === p.name ? t('config.validating') : t('config.validateKey')}
|
||||
</button>
|
||||
{isValidationTarget && validationStatus.valid && (
|
||||
<button className="sm" onClick={handleSaveProvider}>{t('config.save')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="provider-card-meta" style={{ marginTop: 8 }}>
|
||||
<span className="mono">{p.model || '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user