'use client'; import { useEffect, useState } from 'react'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { AppModal } from '@/components/ui/app-modal'; import { createAuthSource, deleteAuthSource, toggleAuthSource, updateAuthSource, } from '@/features/settings/api/settings'; import type { AuthSource, AuthSourcePayload, AuthSourceType, } from '@/features/settings/types'; import { DangerButton, PrimaryButton, ResourceField, ResourceInput, ResourceSelect, SecondaryButton, } from '@/features/shared/components/resource-primitives'; const emptyForm: AuthSourcePayload = { name: '', type: 'github', display_name: '', is_active: false, client_id: '', client_secret: '', openid_discovery_url: '', scopes: 'user:email', icon_url: '', }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function sourceToForm(source: AuthSource): AuthSourcePayload { return { id: source.id, name: source.name, type: source.type, display_name: source.display_name, is_active: source.is_active, client_id: source.client_id, client_secret: '', openid_discovery_url: source.openid_discovery_url, scopes: source.scopes || (source.type === 'oidc' ? 'openid profile email' : 'user:email'), icon_url: source.icon_url, }; } function buildCallbackURL(origin: string, sourceName: string) { const normalizedName = sourceName.trim() || '认证源名称'; return `${origin || '当前访问地址'}/oauth/${encodeURIComponent(normalizedName)}`; } export function AuthSourceModal({ isOpen, sources, isLoading, error, onClose, onChanged, }: { isOpen: boolean; sources: AuthSource[]; isLoading: boolean; error: unknown; onClose: () => void; onChanged: () => Promise; }) { const [mode, setMode] = useState<'list' | 'edit'>('list'); const [editingSource, setEditingSource] = useState(null); const [form, setForm] = useState(emptyForm); const [busyKey, setBusyKey] = useState(null); const [message, setMessage] = useState<{ tone: 'success' | 'danger' | 'info'; text: string; } | null>(null); const [browserOrigin, setBrowserOrigin] = useState(''); useEffect(() => { if (!isOpen) { setMode('list'); setEditingSource(null); setForm(emptyForm); setBusyKey(null); setMessage(null); } }, [isOpen]); useEffect(() => { if (typeof window !== 'undefined') { setBrowserOrigin(window.location.origin); } }, []); const startCreate = () => { setEditingSource(null); setForm(emptyForm); setMessage(null); setMode('edit'); }; const startEdit = (source: AuthSource) => { setEditingSource(source); setForm(sourceToForm(source)); setMessage(null); setMode('edit'); }; const updateType = (type: AuthSourceType) => { setForm((previous) => ({ ...previous, type, scopes: previous.scopes === 'user:email' || previous.scopes === 'openid profile email' || !previous.scopes ? type === 'oidc' ? 'openid profile email' : 'user:email' : previous.scopes, })); }; const runAction = async (key: string, action: () => Promise) => { setBusyKey(key); setMessage(null); try { await action(); } catch (actionError) { setMessage({ tone: 'danger', text: getErrorMessage(actionError) }); } finally { setBusyKey(null); } }; const saveForm = () => { void runAction('save', async () => { const payload: AuthSourcePayload = { ...form, name: form.name.trim(), display_name: form.display_name.trim(), client_id: form.client_id.trim(), client_secret: form.client_secret.trim(), openid_discovery_url: form.openid_discovery_url.trim(), scopes: form.scopes.trim(), icon_url: form.icon_url.trim(), }; if (editingSource) { await updateAuthSource(editingSource.id, payload); } else { await createAuthSource(payload); } await onChanged(); setMessage({ tone: 'success', text: '认证源已保存。' }); setMode('list'); setEditingSource(null); setForm(emptyForm); }); }; const removeSource = (source: AuthSource) => { if ( !window.confirm( `确定删除认证源「${source.display_name || source.name}」吗?`, ) ) { return; } void runAction(`delete-${source.id}`, async () => { await deleteAuthSource(source.id); await onChanged(); setMessage({ tone: 'success', text: '认证源已删除。' }); }); }; const toggleSource = (source: AuthSource) => { void runAction(`toggle-${source.id}`, async () => { await toggleAuthSource(source.id, !source.is_active); await onChanged(); setMessage({ tone: 'success', text: '认证源状态已更新。' }); }); }; return ( setMode('list')}> 返回列表 {busyKey === 'save' ? '保存中...' : '保存认证源'} ) : (
关闭
) } >
{message ? ( ) : null} {mode === 'list' ? (
新增认证源
{isLoading ? : null} {error ? ( ) : null} {!isLoading && !error && sources.length === 0 ? (
暂无认证源。
) : null} {!isLoading && !error && sources.length > 0 ? (
{sources.map((source) => ( ))}
名称 类型 状态 Client ID 操作
{source.display_name || source.name}
{source.name}
{source.type} {source.is_active ? '已启用' : '已禁用'} {source.client_id || '-'}
toggleSource(source)} disabled={busyKey === `toggle-${source.id}`} > {source.is_active ? '禁用' : '启用'} startEdit(source)} > 修改 removeSource(source)} disabled={busyKey === `delete-${source.id}`} > 删除
) : null}
) : (
setForm((previous) => ({ ...previous, name: event.target.value, })) } placeholder="GitHub" /> setForm((previous) => ({ ...previous, display_name: event.target.value, })) } placeholder="GitHub" /> updateType(event.target.value as AuthSourceType) } > setForm((previous) => ({ ...previous, is_active: event.target.value === 'true', })) } > setForm((previous) => ({ ...previous, client_id: event.target.value, })) } /> setForm((previous) => ({ ...previous, client_secret: event.target.value, })) } />
{form.name.trim() ? ( <> 第三方平台的 Redirect URI / Callback URL 请填写: {' '} {buildCallbackURL(browserOrigin, form.name)} 。末尾路径使用上方填写的认证源名称,保存后如修改认证源名称,也需要同步更新第三方平台中的回调地址。 ) : ( '请先输入认证源名称获取回调地址' )}
{form.type === 'oidc' ? ( setForm((previous) => ({ ...previous, openid_discovery_url: event.target.value, })) } placeholder="https://auth.example.com/.well-known/openid-configuration" /> ) : null} setForm((previous) => ({ ...previous, scopes: event.target.value, })) } /> setForm((previous) => ({ ...previous, icon_url: event.target.value, })) } />
)}
); }