+ {/* 启用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() {
+ )}
+
{/* 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 = '';
}