diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index faeaf89..f9ddf55 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -303,6 +303,14 @@ interface SiteConfig { TurnstileSiteKey?: string; TurnstileSecretKey?: string; DefaultUserTags?: string[]; + EnableOIDCLogin?: boolean; + EnableOIDCRegistration?: boolean; + OIDCIssuer?: string; + OIDCAuthorizationEndpoint?: string; + OIDCTokenEndpoint?: string; + OIDCUserInfoEndpoint?: string; + OIDCClientId?: string; + OIDCClientSecret?: string; } // 视频源数据类型 @@ -4589,6 +4597,14 @@ const SiteConfigComponent = ({ TurnstileSiteKey: '', TurnstileSecretKey: '', DefaultUserTags: [], + EnableOIDCLogin: false, + EnableOIDCRegistration: false, + OIDCIssuer: '', + OIDCAuthorizationEndpoint: '', + OIDCTokenEndpoint: '', + OIDCUserInfoEndpoint: '', + OIDCClientId: '', + OIDCClientSecret: '', }); // 豆瓣数据源相关状态 @@ -4662,6 +4678,14 @@ const SiteConfigComponent = ({ TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', TurnstileSecretKey: config.SiteConfig.TurnstileSecretKey || '', DefaultUserTags: config.SiteConfig.DefaultUserTags || [], + EnableOIDCLogin: config.SiteConfig.EnableOIDCLogin || false, + EnableOIDCRegistration: config.SiteConfig.EnableOIDCRegistration || false, + OIDCIssuer: config.SiteConfig.OIDCIssuer || '', + OIDCAuthorizationEndpoint: config.SiteConfig.OIDCAuthorizationEndpoint || '', + OIDCTokenEndpoint: config.SiteConfig.OIDCTokenEndpoint || '', + OIDCUserInfoEndpoint: config.SiteConfig.OIDCUserInfoEndpoint || '', + OIDCClientId: config.SiteConfig.OIDCClientId || '', + OIDCClientSecret: config.SiteConfig.OIDCClientSecret || '', }); } }, [config]); @@ -5412,6 +5436,293 @@ const SiteConfigComponent = ({ + {/* OIDC配置 */} +
+

+ OIDC配置 +

+ + {/* 启用OIDC登录 */} +
+
+ + +
+

+ 开启后登录页面将显示OIDC登录按钮 +

+
+ + {/* 启用OIDC注册 */} +
+
+ + +
+

+ 开启后允许通过OIDC方式注册新用户(需要先启用OIDC登录) +

+
+ + {/* OIDC Issuer */} +
+ +
+ + setSiteSettings((prev) => ({ + ...prev, + OIDCIssuer: e.target.value, + })) + } + className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> + +
+

+ OIDC提供商的Issuer URL,填写后可点击"自动发现"按钮自动获取端点配置 +

+
+ + {/* Authorization Endpoint */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + OIDCAuthorizationEndpoint: e.target.value, + })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +

+ 用户授权的端点URL +

+
+ + {/* Token Endpoint */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + OIDCTokenEndpoint: e.target.value, + })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +

+ 交换授权码获取token的端点URL +

+
+ + {/* UserInfo Endpoint */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + OIDCUserInfoEndpoint: e.target.value, + })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +

+ 获取用户信息的端点URL +

+
+ + {/* OIDC Client ID */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + OIDCClientId: e.target.value, + })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +

+ 在OIDC提供商处注册应用后获得的Client ID +

+
+ + {/* OIDC Client Secret */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + OIDCClientSecret: e.target.value, + })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +

+ 在OIDC提供商处注册应用后获得的Client Secret +

+
+ + {/* OIDC Redirect URI - 只读显示 */} +
+ +
+ + +
+

+ 这是系统自动生成的回调地址,基于环境变量SITE_BASE。请在OIDC提供商(如Keycloak、Auth0等)的应用配置中添加此地址作为允许的重定向URI +

+
+
+ {/* 操作按钮 */}
)} + + {/* OIDC登录按钮 */} + {siteConfig?.EnableOIDCLogin && shouldAskUsername && ( +
+
+
+
+
+
+ + 或 + +
+
+ +
+ )} {/* 版本信息显示 */} diff --git a/src/app/oidc-register/page.tsx b/src/app/oidc-register/page.tsx new file mode 100644 index 0000000..93215a8 --- /dev/null +++ b/src/app/oidc-register/page.tsx @@ -0,0 +1,170 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { Suspense, useEffect, useState } from 'react'; + +import { CURRENT_VERSION } from '@/lib/version'; + +import { useSite } from '@/components/SiteProvider'; +import { ThemeToggle } from '@/components/ThemeToggle'; + +function OIDCRegisterPageClient() { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [oidcInfo, setOidcInfo] = useState(null); + + const { siteName } = useSite(); + + // 检查OIDC session + useEffect(() => { + const checkSession = async () => { + try { + const res = await fetch('/api/auth/oidc/session-info'); + if (res.ok) { + const data = await res.json(); + setOidcInfo(data); + } else { + // session无效,跳转到登录页 + router.replace('/login?error=' + encodeURIComponent('OIDC会话已过期')); + } + } catch (error) { + console.error('检查session失败:', error); + router.replace('/login'); + } + }; + + checkSession(); + }, [router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!username) { + setError('请输入用户名'); + return; + } + + try { + setLoading(true); + const res = await fetch('/api/auth/oidc/complete-register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username }), + }); + + if (res.ok) { + // 注册成功,跳转到首页 + router.replace('/'); + } else { + const data = await res.json().catch(() => ({})); + setError(data.error || '注册失败'); + } + } catch (error) { + setError('网络错误,请稍后重试'); + } finally { + setLoading(false); + } + }; + + if (!oidcInfo) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+
+ +
+
+

+ {siteName} +

+

+ 完成OIDC注册 +

+ + {/* OIDC信息显示 */} + {oidcInfo && ( +
+

+ {oidcInfo.email && ( + <> + 邮箱: {oidcInfo.email} +
+ + )} + {oidcInfo.name && ( + <> + 名称: {oidcInfo.name} + + )} +

+
+ )} + +
+
+ + setUsername(e.target.value)} + /> +

+ 用户名只能包含字母、数字、下划线,长度3-20位 +

+
+ + {error && ( +

{error}

+ )} + + + + {/* 返回登录链接 */} +
+ +
+
+
+ + {/* 版本信息 */} +
+ v{CURRENT_VERSION} +
+
+ ); +} + +export default function OIDCRegisterPage() { + return ( + Loading...}> + + + ); +} diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 8d65d0d..6adc805 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -31,6 +31,15 @@ export interface AdminConfig { TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key TurnstileSecretKey?: string; // Cloudflare Turnstile Secret Key DefaultUserTags?: string[]; // 新注册用户的默认用户组 + // OIDC配置 + EnableOIDCLogin?: boolean; // 启用OIDC登录 + EnableOIDCRegistration?: boolean; // 启用OIDC注册 + OIDCIssuer?: string; // OIDC Issuer URL (用于自动发现) + OIDCAuthorizationEndpoint?: string; // 授权端点 + OIDCTokenEndpoint?: string; // Token端点 + OIDCUserInfoEndpoint?: string; // 用户信息端点 + OIDCClientId?: string; // OIDC Client ID + OIDCClientSecret?: string; // OIDC Client Secret }; UserConfig: { Users: { diff --git a/src/middleware.ts b/src/middleware.ts index 408a695..780439e 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -133,6 +133,6 @@ function shouldSkipAuth(pathname: string): boolean { // 配置middleware匹配规则 export const config = { matcher: [ - '/((?!_next/static|_next/image|favicon.ico|login|register|warning|api/login|api/register|api/logout|api/cron|api/server-config|api/proxy-m3u8|api/cms-proxy|api/tvbox/subscribe|api/theme/css).*)', + '/((?!_next/static|_next/image|favicon.ico|login|register|oidc-register|warning|api/login|api/register|api/logout|api/auth/oidc|api/cron|api/server-config|api/proxy-m3u8|api/cms-proxy|api/tvbox/subscribe|api/theme/css).*)', ], };