From 8678d5fe499bb6ec408fdb344a0552c6813edb65 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 13 Dec 2025 13:03:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=A8=E6=88=B7=E6=B3=A8?= =?UTF-8?q?=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 208 ++++++++++++++++++ src/app/api/admin/site/route.ts | 26 ++- src/app/api/login/route.ts | 55 ++++- src/app/api/register/route.ts | 164 ++++++++++++++ src/app/api/server-config/route.ts | 4 + src/app/login/page.tsx | 87 +++++++- src/app/register/page.tsx | 341 +++++++++++++++++++++++++++++ src/lib/admin.types.ts | 7 + src/middleware.ts | 2 +- 9 files changed, 889 insertions(+), 5 deletions(-) create mode 100644 src/app/api/register/route.ts create mode 100644 src/app/register/page.tsx diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 43001b4..faeaf89 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -297,6 +297,12 @@ interface SiteConfig { DanmakuApiBase: string; DanmakuApiToken: string; EnableComments: boolean; + EnableRegistration?: boolean; + RegistrationRequireTurnstile?: boolean; + LoginRequireTurnstile?: boolean; + TurnstileSiteKey?: string; + TurnstileSecretKey?: string; + DefaultUserTags?: string[]; } // 视频源数据类型 @@ -4577,6 +4583,12 @@ const SiteConfigComponent = ({ DanmakuApiBase: 'http://localhost:9321', DanmakuApiToken: '87654321', EnableComments: false, + EnableRegistration: false, + RegistrationRequireTurnstile: false, + LoginRequireTurnstile: false, + TurnstileSiteKey: '', + TurnstileSecretKey: '', + DefaultUserTags: [], }); // 豆瓣数据源相关状态 @@ -4644,6 +4656,12 @@ const SiteConfigComponent = ({ config.SiteConfig.DanmakuApiBase || 'http://localhost:9321', DanmakuApiToken: config.SiteConfig.DanmakuApiToken || '87654321', EnableComments: config.SiteConfig.EnableComments || false, + EnableRegistration: config.SiteConfig.EnableRegistration || false, + RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, + LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, + TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', + TurnstileSecretKey: config.SiteConfig.TurnstileSecretKey || '', + DefaultUserTags: config.SiteConfig.DefaultUserTags || [], }); } }, [config]); @@ -5204,6 +5222,196 @@ const SiteConfigComponent = ({ + {/* 注册相关配置 */} +
+

+ 注册配置 +

+ + {/* 开启注册 */} +
+
+ + +
+

+ 开启后登录页面将显示注册按钮,允许用户自行注册账号。 +

+
+ + {/* 注册启用Cloudflare Turnstile */} +
+
+ + +
+

+ 开启后注册时需要通过Cloudflare Turnstile人机验证。 +

+
+ + {/* 登录启用Cloudflare Turnstile */} +
+
+ + +
+

+ 开启后登录时需要通过Cloudflare Turnstile人机验证。 +

+
+ + {/* Cloudflare Turnstile Site Key */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + TurnstileSiteKey: 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' + /> +

+ 在Cloudflare Dashboard中获取的Site Key(公钥) +

+
+ + {/* Cloudflare Turnstile Secret Key */} +
+ + + setSiteSettings((prev) => ({ + ...prev, + TurnstileSecretKey: 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' + /> +

+ 在Cloudflare Dashboard中获取的Secret Key(私钥),用于服务端验证 +

+
+ + {/* 默认用户组 */} +
+ + +

+ 新注册的用户将自动分配到选中的用户组,选择"无用户组"为无限制 +

+
+
+ {/* 操作按钮 */}
+ {/* Cloudflare Turnstile */} + {siteConfig?.LoginRequireTurnstile && siteConfig?.TurnstileSiteKey && ( +
+ )} + {error && (

{error}

)} @@ -235,12 +304,26 @@ function LoginPageClient() { + + {/* 注册按钮 */} + {siteConfig?.EnableRegistration && shouldAskUsername && ( +
+ +
+ )} diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..65410d5 --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,341 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +'use client'; + +import { AlertCircle, CheckCircle, Eye, EyeOff } from 'lucide-react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Suspense, useEffect, useState } from 'react'; + +import { CURRENT_VERSION } from '@/lib/version'; +import { checkForUpdates, UpdateStatus } from '@/lib/version_check'; + +import { useSite } from '@/components/SiteProvider'; +import { ThemeToggle } from '@/components/ThemeToggle'; + +// 版本显示组件 +function VersionDisplay() { + const [updateStatus, setUpdateStatus] = useState(null); + const [isChecking, setIsChecking] = useState(true); + + useEffect(() => { + const checkUpdate = async () => { + try { + const status = await checkForUpdates(); + setUpdateStatus(status); + } catch (_) { + // do nothing + } finally { + setIsChecking(false); + } + }; + + checkUpdate(); + }, []); + + return ( + + ); +} + +function RegisterPageClient() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [turnstileToken, setTurnstileToken] = useState(null); + const [turnstileLoaded, setTurnstileLoaded] = useState(false); + const [siteConfig, setSiteConfig] = useState(null); + + const { siteName } = useSite(); + + // 获取站点配置 + useEffect(() => { + const fetchConfig = async () => { + try { + const res = await fetch('/api/server-config'); + if (res.ok) { + const config = await res.json(); + setSiteConfig(config); + + // 如果未开启注册,重定向到登录页 + if (!config.EnableRegistration) { + router.replace('/login'); + } + } + } catch (error) { + console.error('Failed to fetch config:', error); + } + }; + + fetchConfig(); + }, [router]); + + // 加载Cloudflare Turnstile脚本 + useEffect(() => { + if (!siteConfig?.RegistrationRequireTurnstile || !siteConfig?.TurnstileSiteKey) { + return; + } + + const script = document.createElement('script'); + script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'; + script.async = true; + script.defer = true; + script.onload = () => { + setTurnstileLoaded(true); + }; + document.body.appendChild(script); + + return () => { + document.body.removeChild(script); + }; + }, [siteConfig]); + + // 渲染Turnstile组件 + useEffect(() => { + if (!turnstileLoaded || !siteConfig?.TurnstileSiteKey) { + return; + } + + const container = document.getElementById('turnstile-container'); + if (container && (window as any).turnstile) { + (window as any).turnstile.render('#turnstile-container', { + sitekey: siteConfig.TurnstileSiteKey, + callback: (token: string) => { + setTurnstileToken(token); + }, + }); + } + }, [turnstileLoaded, siteConfig]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!username || !password || !confirmPassword) { + setError('请填写所有字段'); + return; + } + + if (password !== confirmPassword) { + setError('两次输入的密码不一致'); + return; + } + + if (password.length < 6) { + setError('密码长度至少为6位'); + return; + } + + // 检查Turnstile验证 + if (siteConfig?.RegistrationRequireTurnstile && !turnstileToken) { + setError('请完成人机验证'); + return; + } + + try { + setLoading(true); + const res = await fetch('/api/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username, + password, + turnstileToken: siteConfig?.RegistrationRequireTurnstile ? turnstileToken : undefined, + }), + }); + + if (res.ok) { + // 注册成功,跳转到登录页 + const redirect = searchParams.get('redirect') || '/login'; + router.replace(redirect); + } else if (res.status === 400) { + const data = await res.json().catch(() => ({})); + setError(data.error || '注册失败'); + } else if (res.status === 409) { + setError('用户名已存在'); + } else { + const data = await res.json().catch(() => ({})); + setError(data.error ?? '服务器错误'); + } + } catch (error) { + setError('网络错误,请稍后重试'); + } finally { + setLoading(false); + } + }; + + // 如果配置未加载或未开启注册,显示加载中 + if (!siteConfig) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+
+ +
+
+

+ {siteName} +

+

+ 创建新账号 +

+
+
+ + setUsername(e.target.value)} + /> +
+ +
+ +
+ setPassword(e.target.value)} + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + /> + +
+
+ + {/* Cloudflare Turnstile */} + {siteConfig?.RegistrationRequireTurnstile && siteConfig?.TurnstileSiteKey && ( +
+ )} + + {error && ( +

{error}

+ )} + + {/* 注册按钮 */} + + + {/* 返回登录链接 */} +
+ +
+
+
+ + {/* 版本信息显示 */} + +
+ ); +} + +export default function RegisterPage() { + return ( + Loading...}> + + + ); +} diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 2181eb1..8d65d0d 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -24,6 +24,13 @@ export interface AdminConfig { // 自定义去广告代码 CustomAdFilterCode?: string; CustomAdFilterVersion?: number; // 代码版本号(时间戳) + // 注册相关配置 + EnableRegistration?: boolean; // 开启注册 + RegistrationRequireTurnstile?: boolean; // 注册启用Cloudflare Turnstile + LoginRequireTurnstile?: boolean; // 登录启用Cloudflare Turnstile + TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key + TurnstileSecretKey?: string; // Cloudflare Turnstile Secret Key + DefaultUserTags?: string[]; // 新注册用户的默认用户组 }; UserConfig: { Users: { diff --git a/src/middleware.ts b/src/middleware.ts index 88d6c6e..408a695 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|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|warning|api/login|api/register|api/logout|api/cron|api/server-config|api/proxy-m3u8|api/cms-proxy|api/tvbox/subscribe|api/theme/css).*)', ], };