增加注册邀请码功能

This commit is contained in:
mtvpls
2026-03-31 12:53:19 +08:00
parent 5e1df4c21a
commit bf17abacf3
8 changed files with 409 additions and 224 deletions
+304 -223
View File
@@ -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 = ({
</h3>
{/* 开启注册 */}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<button
type='button'
onClick={() => handleRegistrationToggle(!registrationSettings.EnableRegistration)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
registrationSettings.EnableRegistration
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.EnableRegistration
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
<details open className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
</summary>
<div className='mt-4 space-y-4'>
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<button
type='button'
onClick={() => handleRegistrationToggle(!registrationSettings.EnableRegistration)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
registrationSettings.EnableRegistration
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.EnableRegistration
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<select
value={registrationSettings.DefaultUserTags && registrationSettings.DefaultUserTags.length > 0 ? registrationSettings.DefaultUserTags[0] : ''}
onChange={(e) => {
const value = e.target.value;
setRegistrationSettings((prev) => ({
...prev,
DefaultUserTags: value ? [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'
>
<option value=''></option>
{config?.UserConfig?.Tags && config.UserConfig.Tags.map((tag) => (
<option key={tag.name} value={tag.name}>
{tag.name}
{tag.enabledApis && tag.enabledApis.length > 0
? ` (${tag.enabledApis.length} 个源)`
: ''}
</option>
))}
</select>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
"无用户组"
</p>
</div>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
</details>
{/* 注册启用Cloudflare Turnstile */}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile
</label>
<button
type='button'
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
onClick={() =>
setRegistrationSettings((prev) => ({
...prev,
RegistrationRequireTurnstile: !prev.RegistrationRequireTurnstile,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600'
: registrationSettings.RegistrationRequireTurnstile
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.RegistrationRequireTurnstile
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
</summary>
<div className='mt-4 space-y-4'>
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<button
type='button'
onClick={() =>
setRegistrationSettings((prev) => ({
...prev,
RequireRegistrationInviteCode: !prev.RequireRegistrationInviteCode,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
registrationSettings.RequireRegistrationInviteCode
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.RequireRegistrationInviteCode
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='text'
placeholder='请输入通用注册邀请码'
value={registrationSettings.RegistrationInviteCode || ''}
onChange={(e) =>
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'
/>
</button>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Turnstile人机验证
{(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && (
<span className='text-orange-500 dark:text-orange-400'> Site Key和Secret Key才能启用</span>
)}
</p>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
{/* 登录启用Cloudflare Turnstile */}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile
</label>
<button
type='button'
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
onClick={() =>
setRegistrationSettings((prev) => ({
...prev,
LoginRequireTurnstile: !prev.LoginRequireTurnstile,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600'
: registrationSettings.LoginRequireTurnstile
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.LoginRequireTurnstile
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile
</label>
<button
type='button'
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
onClick={() =>
setRegistrationSettings((prev) => ({
...prev,
RegistrationRequireTurnstile: !prev.RegistrationRequireTurnstile,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600'
: registrationSettings.RegistrationRequireTurnstile
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.RegistrationRequireTurnstile
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Turnstile人机验证
{(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && (
<span className='text-orange-500 dark:text-orange-400'> Site Key和Secret Key才能启用</span>
)}
</p>
</div>
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile
</label>
<button
type='button'
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
onClick={() =>
setRegistrationSettings((prev) => ({
...prev,
LoginRequireTurnstile: !prev.LoginRequireTurnstile,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600'
: registrationSettings.LoginRequireTurnstile
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
registrationSettings.LoginRequireTurnstile
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Turnstile人机验证
{(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && (
<span className='text-orange-500 dark:text-orange-400'> Site Key和Secret Key才能启用</span>
)}
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile Site Key
</label>
<input
type='text'
placeholder='请输入Cloudflare Turnstile Site Key'
value={registrationSettings.TurnstileSiteKey || ''}
onChange={(e) =>
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'
/>
</button>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Dashboard中获取的Site Key
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile Secret Key
</label>
<input
type='password'
placeholder='请输入Cloudflare Turnstile Secret Key'
value={registrationSettings.TurnstileSecretKey || ''}
onChange={(e) =>
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'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Dashboard中获取的Secret Key
</p>
</div>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Turnstile人机验证
{(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && (
<span className='text-orange-500 dark:text-orange-400'> Site Key和Secret Key才能启用</span>
)}
</p>
</div>
{/* Cloudflare Turnstile Site Key */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile Site Key
</label>
<input
type='text'
placeholder='请输入Cloudflare Turnstile Site Key'
value={registrationSettings.TurnstileSiteKey || ''}
onChange={(e) =>
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'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Dashboard中获取的Site Key
</p>
</div>
{/* Cloudflare Turnstile Secret Key */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Cloudflare Turnstile Secret Key
</label>
<input
type='password'
placeholder='请输入Cloudflare Turnstile Secret Key'
value={registrationSettings.TurnstileSecretKey || ''}
onChange={(e) =>
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'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Cloudflare Dashboard中获取的Secret Key
</p>
</div>
{/* 默认用户组 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<select
value={registrationSettings.DefaultUserTags && registrationSettings.DefaultUserTags.length > 0 ? registrationSettings.DefaultUserTags[0] : ''}
onChange={(e) => {
const value = e.target.value;
setRegistrationSettings((prev) => ({
...prev,
DefaultUserTags: value ? [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'
>
<option value=''></option>
{config?.UserConfig?.Tags && config.UserConfig.Tags.map((tag) => (
<option key={tag.name} value={tag.name}>
{tag.name}
{tag.enabledApis && tag.enabledApis.length > 0
? ` (${tag.enabledApis.length} 个源)`
: ''}
</option>
))}
</select>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
"无用户组"
</p>
</div>
</details>
</div>
{/* OIDC配置 */}
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'>
<details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
OIDC配置
</h3>
{/* 启用OIDC登录 */}
<div>
</summary>
<div className='mt-4 space-y-4'>
{/* 启用OIDC登录 */}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC登录
@@ -9413,10 +9493,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC登录按钮
</p>
</div>
</div>
{/* 启用OIDC注册 */}
<div>
{/* 启用OIDC注册 */}
<div>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC注册
@@ -9449,10 +9529,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC方式注册新用户OIDC登录
</p>
</div>
</div>
{/* OIDC Issuer */}
<div>
{/* OIDC Issuer */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Issuer URL
</label>
@@ -9514,10 +9594,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商的Issuer URL"自动发现"
</p>
</div>
</div>
{/* Authorization Endpoint */}
<div>
{/* Authorization Endpoint */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Authorization Endpoint
</label>
@@ -9536,10 +9616,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
URL
</p>
</div>
</div>
{/* Token Endpoint */}
<div>
{/* Token Endpoint */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Token EndpointToken端点
</label>
@@ -9558,10 +9638,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
token的端点URL
</p>
</div>
</div>
{/* UserInfo Endpoint */}
<div>
{/* UserInfo Endpoint */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
UserInfo Endpoint
</label>
@@ -9580,10 +9660,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
URL
</p>
</div>
</div>
{/* OIDC Client ID */}
<div>
{/* OIDC Client ID */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Client ID
</label>
@@ -9602,10 +9682,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商处注册应用后获得的Client ID
</p>
</div>
</div>
{/* OIDC Client Secret */}
<div>
{/* OIDC Client Secret */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Client Secret
</label>
@@ -9624,10 +9704,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商处注册应用后获得的Client Secret
</p>
</div>
</div>
{/* OIDC Redirect URI - 只读显示 */}
<div>
{/* OIDC Redirect URI - 只读显示 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Redirect URI
</label>
@@ -9657,10 +9737,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
SITE_BASEOIDC提供商KeycloakAuth0等URI
</p>
</div>
</div>
{/* OIDC登录按钮文字 */}
<div>
{/* OIDC登录按钮文字 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC登录按钮文字
</label>
@@ -9679,10 +9759,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC登录按钮显示的文字,"使用企业账号登录""使用SSO登录""使用OIDC登录"
</p>
</div>
</div>
{/* OIDC最低信任等级 */}
<div>
{/* OIDC最低信任等级 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
@@ -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'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
LinuxDo网站有效01-4
</p>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
LinuxDo网站有效01-4
</p>
</div>
</div>
</div>
</details>
{/* 操作按钮 */}
<div className='flex justify-end'>
+8
View File
@@ -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,
+21 -1
View File
@@ -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 {
+1
View File
@@ -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 || '',
+3
View File
@@ -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,
+30
View File
@@ -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<string | null>(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() {
</div>
</div>
{siteConfig?.RequireRegistrationInviteCode && (
<div>
<label htmlFor='inviteCode' className='sr-only'>
</label>
<div className='relative'>
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
<User className='h-5 w-5 text-gray-400 dark:text-gray-500' />
</div>
<input
id='inviteCode'
type='text'
className='block w-full rounded-lg border-0 py-3 pl-10 pr-4 text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-white/60 dark:ring-white/20 placeholder:text-gray-500 dark:placeholder:text-gray-400 focus:ring-2 focus:ring-green-500 focus:outline-none sm:text-base bg-white/60 dark:bg-zinc-800/60'
placeholder='输入邀请码'
value={inviteCode}
onChange={(e) => setInviteCode(e.target.value)}
/>
</div>
</div>
)}
{/* Cloudflare Turnstile */}
{siteConfig?.RegistrationRequireTurnstile && siteConfig?.TurnstileSiteKey && (
<div id='turnstile-container' className='flex justify-center'></div>
@@ -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'
+2
View File
@@ -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
+40
View File
@@ -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 = '';
}