diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 52c4af0..4721044 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -358,6 +358,8 @@ interface SiteConfig { MagnetAcgripReverseProxy?: string; EnableComments: boolean; EnableRegistration?: boolean; + RequireRegistrationInviteCode?: boolean; + RegistrationInviteCode?: string; RegistrationRequireTurnstile?: boolean; LoginRequireTurnstile?: boolean; TurnstileSiteKey?: string; @@ -9054,6 +9056,8 @@ const RegistrationConfigComponent = ({ const [showEnableRegistrationModal, setShowEnableRegistrationModal] = useState(false); const [registrationSettings, setRegistrationSettings] = useState<{ EnableRegistration: boolean; + RequireRegistrationInviteCode: boolean; + RegistrationInviteCode: string; RegistrationRequireTurnstile: boolean; LoginRequireTurnstile: boolean; TurnstileSiteKey: string; @@ -9071,6 +9075,8 @@ const RegistrationConfigComponent = ({ OIDCMinTrustLevel: number; }>({ EnableRegistration: false, + RequireRegistrationInviteCode: false, + RegistrationInviteCode: '', RegistrationRequireTurnstile: false, LoginRequireTurnstile: false, TurnstileSiteKey: '', @@ -9092,6 +9098,8 @@ const RegistrationConfigComponent = ({ if (config?.SiteConfig) { setRegistrationSettings({ EnableRegistration: config.SiteConfig.EnableRegistration || false, + RequireRegistrationInviteCode: config.SiteConfig.RequireRegistrationInviteCode || false, + RegistrationInviteCode: config.SiteConfig.RegistrationInviteCode || '', RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', @@ -9140,10 +9148,18 @@ const RegistrationConfigComponent = ({ throw new Error('配置未加载'); } + if ( + registrationSettings.RequireRegistrationInviteCode && + !registrationSettings.RegistrationInviteCode.trim() + ) { + throw new Error('已开启注册邀请码时,邀请码不能为空'); + } + // 合并站点配置和注册配置 const updatedSiteConfig = { ...config.SiteConfig, ...registrationSettings, + RegistrationInviteCode: registrationSettings.RegistrationInviteCode.trim(), }; const resp = await fetch('/api/admin/site', { @@ -9182,205 +9198,269 @@ const RegistrationConfigComponent = ({ 注册配置 - {/* 开启注册 */} -
-
- - +
+ + 基础注册设置 + +
+
+
+ + +
+

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

+
+ +
+ + +

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

+
-

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

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

+ 开启后,普通注册必须填写管理员设置的统一邀请码。 +

+
+ +
+ + + setRegistrationSettings((prev) => ({ + ...prev, + RegistrationInviteCode: 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 Turnstile人机验证。 - {(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && ( - 需要先配置Site Key和Secret Key才能启用。 - )} -

-
+

+ 仅普通注册生效;开启邀请码注册时不能为空。 +

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

+ 开启后注册时需要通过Cloudflare Turnstile人机验证。 + {(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && ( + 需要先配置Site Key和Secret Key才能启用。 + )} +

+
+ +
+
+ + +
+

+ 开启后登录时需要通过Cloudflare Turnstile人机验证。 + {(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && ( + 需要先配置Site Key和Secret Key才能启用。 + )} +

+
+ +
+ + + setRegistrationSettings((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(公钥) +

+
+ +
+ + + setRegistrationSettings((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人机验证。 - {(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && ( - 需要先配置Site Key和Secret Key才能启用。 - )} -

- - - {/* Cloudflare Turnstile Site Key */} -
- - - setRegistrationSettings((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 */} -
- - - setRegistrationSettings((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(私钥),用于服务端验证 -

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

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

-
+ {/* OIDC配置 */} -
-

+
+ OIDC配置 -

- - {/* 启用OIDC登录 */} -
+ +
+ {/* 启用OIDC登录 */} +
+
- {/* 启用OIDC注册 */} -
+ {/* 启用OIDC注册 */} +
+
- {/* OIDC Issuer */} -
+ {/* OIDC Issuer */} +
@@ -9514,10 +9594,10 @@ const RegistrationConfigComponent = ({

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

-
+
- {/* Authorization Endpoint */} -
+ {/* Authorization Endpoint */} +
@@ -9536,10 +9616,10 @@ const RegistrationConfigComponent = ({

用户授权的端点URL

-
+
- {/* Token Endpoint */} -
+ {/* Token Endpoint */} +
@@ -9558,10 +9638,10 @@ const RegistrationConfigComponent = ({

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

-
+
- {/* UserInfo Endpoint */} -
+ {/* UserInfo Endpoint */} +
@@ -9580,10 +9660,10 @@ const RegistrationConfigComponent = ({

获取用户信息的端点URL

-
+
- {/* OIDC Client ID */} -
+ {/* OIDC Client ID */} +
@@ -9602,10 +9682,10 @@ const RegistrationConfigComponent = ({

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

-
+
- {/* OIDC Client Secret */} -
+ {/* OIDC Client Secret */} +
@@ -9624,10 +9704,10 @@ const RegistrationConfigComponent = ({

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

-
+
- {/* OIDC Redirect URI - 只读显示 */} -
+ {/* OIDC Redirect URI - 只读显示 */} +
@@ -9657,10 +9737,10 @@ const RegistrationConfigComponent = ({

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

-
+
- {/* OIDC登录按钮文字 */} -
+ {/* OIDC登录按钮文字 */} +
@@ -9679,10 +9759,10 @@ const RegistrationConfigComponent = ({

自定义OIDC登录按钮显示的文字,如"使用企业账号登录"、"使用SSO登录"等。留空则显示默认文字"使用OIDC登录"

-
+
- {/* OIDC最低信任等级 */} -
+ {/* OIDC最低信任等级 */} +
@@ -9700,11 +9780,12 @@ const RegistrationConfigComponent = ({ } 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' /> -

- 仅LinuxDo网站有效。设置为0时不判断,1-4表示最低信任等级要求 -

+

+ 仅LinuxDo网站有效。设置为0时不判断,1-4表示最低信任等级要求 +

+
-
+ {/* 操作按钮 */}
diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts index 6619bff..75ce279 100644 --- a/src/app/api/admin/site/route.ts +++ b/src/app/api/admin/site/route.ts @@ -58,6 +58,8 @@ export async function POST(request: NextRequest) { CustomAdFilterCode, CustomAdFilterVersion, EnableRegistration, + RequireRegistrationInviteCode, + RegistrationInviteCode, RegistrationRequireTurnstile, LoginRequireTurnstile, TurnstileSiteKey, @@ -103,6 +105,8 @@ export async function POST(request: NextRequest) { CustomAdFilterCode?: string; CustomAdFilterVersion?: number; EnableRegistration?: boolean; + RequireRegistrationInviteCode?: boolean; + RegistrationInviteCode?: string; RegistrationRequireTurnstile?: boolean; LoginRequireTurnstile?: boolean; TurnstileSiteKey?: string; @@ -148,6 +152,8 @@ export async function POST(request: NextRequest) { (CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') || (CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') || (EnableRegistration !== undefined && typeof EnableRegistration !== 'boolean') || + (RequireRegistrationInviteCode !== undefined && typeof RequireRegistrationInviteCode !== 'boolean') || + (RegistrationInviteCode !== undefined && typeof RegistrationInviteCode !== 'string') || (RegistrationRequireTurnstile !== undefined && typeof RegistrationRequireTurnstile !== 'boolean') || (LoginRequireTurnstile !== undefined && typeof LoginRequireTurnstile !== 'boolean') || (TurnstileSiteKey !== undefined && typeof TurnstileSiteKey !== 'string') || @@ -208,6 +214,8 @@ export async function POST(request: NextRequest) { CustomAdFilterCode, CustomAdFilterVersion, EnableRegistration, + RequireRegistrationInviteCode, + RegistrationInviteCode, RegistrationRequireTurnstile, LoginRequireTurnstile, TurnstileSiteKey, diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index 543df9f..ea0ad05 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -60,7 +60,7 @@ export async function POST(req: NextRequest) { ); } - const { username, password, turnstileToken } = await req.json(); + const { username, password, inviteCode, turnstileToken } = await req.json(); // 验证输入 if (!username || typeof username !== 'string') { @@ -69,6 +69,9 @@ export async function POST(req: NextRequest) { if (!password || typeof password !== 'string') { return NextResponse.json({ error: '密码不能为空' }, { status: 400 }); } + if (inviteCode !== undefined && typeof inviteCode !== 'string') { + return NextResponse.json({ error: '邀请码格式错误' }, { status: 400 }); + } // 验证用户名格式(只允许字母、数字、下划线,长度3-20) if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { @@ -94,6 +97,23 @@ export async function POST(req: NextRequest) { ); } + if (siteConfig.RequireRegistrationInviteCode) { + const expectedInviteCode = (siteConfig.RegistrationInviteCode || '').trim(); + if (!expectedInviteCode) { + return NextResponse.json( + { error: '服务器未配置邀请码' }, + { status: 500 } + ); + } + + if (!inviteCode || inviteCode.trim() !== expectedInviteCode) { + return NextResponse.json( + { error: '邀请码错误' }, + { status: 400 } + ); + } + } + // 获取用户名锁,防止并发注册 let releaseLock: (() => void) | null = null; try { diff --git a/src/app/api/server-config/route.ts b/src/app/api/server-config/route.ts index def5431..f9b9ff0 100644 --- a/src/app/api/server-config/route.ts +++ b/src/app/api/server-config/route.ts @@ -50,6 +50,7 @@ export async function GET(request: NextRequest) { WatchRoom: watchRoomConfig, EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true', EnableRegistration: config.SiteConfig.EnableRegistration || false, + RequireRegistrationInviteCode: config.SiteConfig.RequireRegistrationInviteCode || false, RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cf54887..22eb3a6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -75,6 +75,7 @@ export default async function RootLayout({ let progressThumbPresetId = ''; let progressThumbCustomUrl = ''; let enableRegistration = false; + let requireRegistrationInviteCode = false; let loginRequireTurnstile = false; let registrationRequireTurnstile = false; let turnstileSiteKey = ''; @@ -125,6 +126,7 @@ export default async function RootLayout({ progressThumbPresetId = config.ThemeConfig?.progressThumbPresetId || ''; progressThumbCustomUrl = config.ThemeConfig?.progressThumbCustomUrl || ''; enableRegistration = config.SiteConfig.EnableRegistration || false; + requireRegistrationInviteCode = config.SiteConfig.RequireRegistrationInviteCode || false; loginRequireTurnstile = config.SiteConfig.LoginRequireTurnstile || false; registrationRequireTurnstile = config.SiteConfig.RegistrationRequireTurnstile || false; turnstileSiteKey = config.SiteConfig.TurnstileSiteKey || ''; @@ -195,6 +197,7 @@ export default async function RootLayout({ PROGRESS_THUMB_PRESET_ID: progressThumbPresetId, PROGRESS_THUMB_CUSTOM_URL: progressThumbCustomUrl, ENABLE_REGISTRATION: enableRegistration, + REQUIRE_REGISTRATION_INVITE_CODE: requireRegistrationInviteCode, LOGIN_REQUIRE_TURNSTILE: loginRequireTurnstile, REGISTRATION_REQUIRE_TURNSTILE: registrationRequireTurnstile, TURNSTILE_SITE_KEY: turnstileSiteKey, diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 0e19786..030a323 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -73,6 +73,7 @@ function RegisterPageClient() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [inviteCode, setInviteCode] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); @@ -108,6 +109,7 @@ function RegisterPageClient() { // 设置站点配置 const config = { EnableRegistration: runtimeConfig?.ENABLE_REGISTRATION || false, + RequireRegistrationInviteCode: runtimeConfig?.REQUIRE_REGISTRATION_INVITE_CODE || false, RegistrationRequireTurnstile: runtimeConfig?.REGISTRATION_REQUIRE_TURNSTILE || false, TurnstileSiteKey: runtimeConfig?.TURNSTILE_SITE_KEY || '', }; @@ -167,6 +169,11 @@ function RegisterPageClient() { return; } + if (siteConfig?.RequireRegistrationInviteCode && !inviteCode.trim()) { + setError('请输入邀请码'); + return; + } + if (password !== confirmPassword) { setError('两次输入的密码不一致'); return; @@ -191,6 +198,7 @@ function RegisterPageClient() { body: JSON.stringify({ username, password, + inviteCode: siteConfig?.RequireRegistrationInviteCode ? inviteCode.trim() : undefined, turnstileToken: siteConfig?.RegistrationRequireTurnstile ? turnstileToken : undefined, }), }); @@ -340,6 +348,27 @@ function RegisterPageClient() {
+ {siteConfig?.RequireRegistrationInviteCode && ( +
+ +
+
+ +
+ setInviteCode(e.target.value)} + /> +
+
+ )} + {/* Cloudflare Turnstile */} {siteConfig?.RegistrationRequireTurnstile && siteConfig?.TurnstileSiteKey && (
@@ -354,6 +383,7 @@ function RegisterPageClient() { type='submit' disabled={ !username || !password || !confirmPassword || loading || + (siteConfig?.RequireRegistrationInviteCode && !inviteCode.trim()) || (siteConfig?.RegistrationRequireTurnstile && !turnstileToken) } className='inline-flex w-full justify-center rounded-lg bg-green-600 py-3 text-base font-semibold text-white shadow-lg transition-all duration-200 hover:from-green-600 hover:to-blue-600 disabled:cursor-not-allowed disabled:opacity-50' diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index eb74fc8..117f797 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -42,6 +42,8 @@ export interface AdminConfig { CustomAdFilterVersion?: number; // 代码版本号(时间戳) // 注册相关配置 EnableRegistration?: boolean; // 开启注册 + RequireRegistrationInviteCode?: boolean; // 注册时要求邀请码 + RegistrationInviteCode?: string; // 通用注册邀请码 RegistrationRequireTurnstile?: boolean; // 注册启用Cloudflare Turnstile LoginRequireTurnstile?: boolean; // 登录启用Cloudflare Turnstile TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key diff --git a/src/lib/config.ts b/src/lib/config.ts index 6216130..25090aa 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -263,6 +263,14 @@ async function getInitConfig(configFile: string, subConfig: { MagnetAcgripReverseProxy: '', // 评论功能开关 EnableComments: false, + EnableRegistration: false, + RequireRegistrationInviteCode: false, + RegistrationInviteCode: '', + RegistrationRequireTurnstile: false, + LoginRequireTurnstile: false, + TurnstileSiteKey: '', + TurnstileSecretKey: '', + DefaultUserTags: [], }, UserConfig: { Users: [], @@ -442,6 +450,14 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { MagnetDmhyReverseProxy: '', MagnetAcgripReverseProxy: '', EnableComments: false, + EnableRegistration: false, + RequireRegistrationInviteCode: false, + RegistrationInviteCode: '', + RegistrationRequireTurnstile: false, + LoginRequireTurnstile: false, + TurnstileSiteKey: '', + TurnstileSecretKey: '', + DefaultUserTags: [], }; } // 确保弹幕配置存在 @@ -455,6 +471,30 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { if (adminConfig.SiteConfig.EnableComments === undefined) { adminConfig.SiteConfig.EnableComments = false; } + if (adminConfig.SiteConfig.EnableRegistration === undefined) { + adminConfig.SiteConfig.EnableRegistration = false; + } + if (adminConfig.SiteConfig.RequireRegistrationInviteCode === undefined) { + adminConfig.SiteConfig.RequireRegistrationInviteCode = false; + } + if (adminConfig.SiteConfig.RegistrationInviteCode === undefined) { + adminConfig.SiteConfig.RegistrationInviteCode = ''; + } + if (adminConfig.SiteConfig.RegistrationRequireTurnstile === undefined) { + adminConfig.SiteConfig.RegistrationRequireTurnstile = false; + } + if (adminConfig.SiteConfig.LoginRequireTurnstile === undefined) { + adminConfig.SiteConfig.LoginRequireTurnstile = false; + } + if (adminConfig.SiteConfig.TurnstileSiteKey === undefined) { + adminConfig.SiteConfig.TurnstileSiteKey = ''; + } + if (adminConfig.SiteConfig.TurnstileSecretKey === undefined) { + adminConfig.SiteConfig.TurnstileSecretKey = ''; + } + if (adminConfig.SiteConfig.DefaultUserTags === undefined) { + adminConfig.SiteConfig.DefaultUserTags = []; + } if (adminConfig.SiteConfig.PansouKeywordBlocklist === undefined) { adminConfig.SiteConfig.PansouKeywordBlocklist = ''; }