增加注册邀请码功能

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; MagnetAcgripReverseProxy?: string;
EnableComments: boolean; EnableComments: boolean;
EnableRegistration?: boolean; EnableRegistration?: boolean;
RequireRegistrationInviteCode?: boolean;
RegistrationInviteCode?: string;
RegistrationRequireTurnstile?: boolean; RegistrationRequireTurnstile?: boolean;
LoginRequireTurnstile?: boolean; LoginRequireTurnstile?: boolean;
TurnstileSiteKey?: string; TurnstileSiteKey?: string;
@@ -9054,6 +9056,8 @@ const RegistrationConfigComponent = ({
const [showEnableRegistrationModal, setShowEnableRegistrationModal] = useState(false); const [showEnableRegistrationModal, setShowEnableRegistrationModal] = useState(false);
const [registrationSettings, setRegistrationSettings] = useState<{ const [registrationSettings, setRegistrationSettings] = useState<{
EnableRegistration: boolean; EnableRegistration: boolean;
RequireRegistrationInviteCode: boolean;
RegistrationInviteCode: string;
RegistrationRequireTurnstile: boolean; RegistrationRequireTurnstile: boolean;
LoginRequireTurnstile: boolean; LoginRequireTurnstile: boolean;
TurnstileSiteKey: string; TurnstileSiteKey: string;
@@ -9071,6 +9075,8 @@ const RegistrationConfigComponent = ({
OIDCMinTrustLevel: number; OIDCMinTrustLevel: number;
}>({ }>({
EnableRegistration: false, EnableRegistration: false,
RequireRegistrationInviteCode: false,
RegistrationInviteCode: '',
RegistrationRequireTurnstile: false, RegistrationRequireTurnstile: false,
LoginRequireTurnstile: false, LoginRequireTurnstile: false,
TurnstileSiteKey: '', TurnstileSiteKey: '',
@@ -9092,6 +9098,8 @@ const RegistrationConfigComponent = ({
if (config?.SiteConfig) { if (config?.SiteConfig) {
setRegistrationSettings({ setRegistrationSettings({
EnableRegistration: config.SiteConfig.EnableRegistration || false, EnableRegistration: config.SiteConfig.EnableRegistration || false,
RequireRegistrationInviteCode: config.SiteConfig.RequireRegistrationInviteCode || false,
RegistrationInviteCode: config.SiteConfig.RegistrationInviteCode || '',
RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false,
LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false,
TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '',
@@ -9140,10 +9148,18 @@ const RegistrationConfigComponent = ({
throw new Error('配置未加载'); throw new Error('配置未加载');
} }
if (
registrationSettings.RequireRegistrationInviteCode &&
!registrationSettings.RegistrationInviteCode.trim()
) {
throw new Error('已开启注册邀请码时,邀请码不能为空');
}
// 合并站点配置和注册配置 // 合并站点配置和注册配置
const updatedSiteConfig = { const updatedSiteConfig = {
...config.SiteConfig, ...config.SiteConfig,
...registrationSettings, ...registrationSettings,
RegistrationInviteCode: registrationSettings.RegistrationInviteCode.trim(),
}; };
const resp = await fetch('/api/admin/site', { const resp = await fetch('/api/admin/site', {
@@ -9182,205 +9198,269 @@ const RegistrationConfigComponent = ({
</h3> </h3>
{/* 开启注册 */} <details open className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<div> <summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> </summary>
<div className='mt-4 space-y-4'>
</label> <div>
<button <div className='flex items-center justify-between'>
type='button' <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
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 ${ </label>
registrationSettings.EnableRegistration <button
? buttonStyles.toggleOn type='button'
: buttonStyles.toggleOff 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
<span ? buttonStyles.toggleOn
className={`inline-block h-4 w-4 transform rounded-full ${ : buttonStyles.toggleOff
buttonStyles.toggleThumb }`}
} transition-transform ${ >
registrationSettings.EnableRegistration <span
? buttonStyles.toggleThumbOn className={`inline-block h-4 w-4 transform rounded-full ${
: buttonStyles.toggleThumbOff buttonStyles.toggleThumb
}`} } transition-transform ${
/> registrationSettings.EnableRegistration
</button> ? 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> </div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> </details>
</p>
</div>
{/* 注册启用Cloudflare Turnstile */} <details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<div> <summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
<div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> </summary>
Cloudflare Turnstile <div className='mt-4 space-y-4'>
</label> <div>
<button <div className='flex items-center justify-between'>
type='button' <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
onClick={() => </label>
setRegistrationSettings((prev) => ({ <button
...prev, type='button'
RegistrationRequireTurnstile: !prev.RegistrationRequireTurnstile, onClick={() =>
})) setRegistrationSettings((prev) => ({
} ...prev,
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 ${ RequireRegistrationInviteCode: !prev.RequireRegistrationInviteCode,
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey }))
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600' }
: registrationSettings.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 ${
? buttonStyles.toggleOn registrationSettings.RequireRegistrationInviteCode
: buttonStyles.toggleOff ? buttonStyles.toggleOn
}`} : buttonStyles.toggleOff
> }`}
<span >
className={`inline-block h-4 w-4 transform rounded-full ${ <span
buttonStyles.toggleThumb className={`inline-block h-4 w-4 transform rounded-full ${
} transition-transform ${ buttonStyles.toggleThumb
registrationSettings.RegistrationRequireTurnstile } transition-transform ${
? buttonStyles.toggleThumbOn registrationSettings.RequireRegistrationInviteCode
: buttonStyles.toggleThumbOff ? 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> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> </p>
Cloudflare Turnstile人机验证 </div>
{(!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey) && (
<span className='text-orange-500 dark:text-orange-400'> Site Key和Secret Key才能启用</span>
)}
</p>
</div>
{/* 登录启用Cloudflare Turnstile */} <div>
<div> <div className='flex items-center justify-between'>
<div className='flex items-center justify-between'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> Cloudflare Turnstile
Cloudflare Turnstile </label>
</label> <button
<button type='button'
type='button' disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey}
disabled={!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey} onClick={() =>
onClick={() => setRegistrationSettings((prev) => ({
setRegistrationSettings((prev) => ({ ...prev,
...prev, RegistrationRequireTurnstile: !prev.RegistrationRequireTurnstile,
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 ${
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
!registrationSettings.TurnstileSiteKey || !registrationSettings.TurnstileSecretKey ? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600'
? 'opacity-50 cursor-not-allowed bg-gray-300 dark:bg-gray-600' : registrationSettings.RegistrationRequireTurnstile
: registrationSettings.LoginRequireTurnstile ? buttonStyles.toggleOn
? buttonStyles.toggleOn : buttonStyles.toggleOff
: buttonStyles.toggleOff }`}
}`} >
> <span
<span className={`inline-block h-4 w-4 transform rounded-full ${
className={`inline-block h-4 w-4 transform rounded-full ${ buttonStyles.toggleThumb
buttonStyles.toggleThumb } transition-transform ${
} transition-transform ${ registrationSettings.RegistrationRequireTurnstile
registrationSettings.LoginRequireTurnstile ? buttonStyles.toggleThumbOn
? buttonStyles.toggleThumbOn : buttonStyles.toggleThumbOff
: 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> </div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> </details>
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>
</div> </div>
{/* OIDC配置 */} {/* OIDC配置 */}
<div className='space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700'> <details className='pt-4 border-t border-gray-200 dark:border-gray-700'>
<h3 className='text-sm font-semibold text-gray-900 dark:text-gray-100'> <summary className='text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer'>
OIDC配置 OIDC配置
</h3> </summary>
<div className='mt-4 space-y-4'>
{/* 启用OIDC登录 */} {/* 启用OIDC登录 */}
<div> <div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC登录 OIDC登录
@@ -9413,10 +9493,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC登录按钮 OIDC登录按钮
</p> </p>
</div> </div>
{/* 启用OIDC注册 */} {/* 启用OIDC注册 */}
<div> <div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC注册 OIDC注册
@@ -9449,10 +9529,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC方式注册新用户OIDC登录 OIDC方式注册新用户OIDC登录
</p> </p>
</div> </div>
{/* OIDC Issuer */} {/* OIDC Issuer */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Issuer URL OIDC Issuer URL
</label> </label>
@@ -9514,10 +9594,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商的Issuer URL"自动发现" OIDC提供商的Issuer URL"自动发现"
</p> </p>
</div> </div>
{/* Authorization Endpoint */} {/* Authorization Endpoint */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Authorization Endpoint Authorization Endpoint
</label> </label>
@@ -9536,10 +9616,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
URL URL
</p> </p>
</div> </div>
{/* Token Endpoint */} {/* Token Endpoint */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
Token EndpointToken端点 Token EndpointToken端点
</label> </label>
@@ -9558,10 +9638,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
token的端点URL token的端点URL
</p> </p>
</div> </div>
{/* UserInfo Endpoint */} {/* UserInfo Endpoint */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
UserInfo Endpoint UserInfo Endpoint
</label> </label>
@@ -9580,10 +9660,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
URL URL
</p> </p>
</div> </div>
{/* OIDC Client ID */} {/* OIDC Client ID */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Client ID OIDC Client ID
</label> </label>
@@ -9602,10 +9682,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商处注册应用后获得的Client ID OIDC提供商处注册应用后获得的Client ID
</p> </p>
</div> </div>
{/* OIDC Client Secret */} {/* OIDC Client Secret */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Client Secret OIDC Client Secret
</label> </label>
@@ -9624,10 +9704,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC提供商处注册应用后获得的Client Secret OIDC提供商处注册应用后获得的Client Secret
</p> </p>
</div> </div>
{/* OIDC Redirect URI - 只读显示 */} {/* OIDC Redirect URI - 只读显示 */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC Redirect URI OIDC Redirect URI
</label> </label>
@@ -9657,10 +9737,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
SITE_BASEOIDC提供商KeycloakAuth0等URI SITE_BASEOIDC提供商KeycloakAuth0等URI
</p> </p>
</div> </div>
{/* OIDC登录按钮文字 */} {/* OIDC登录按钮文字 */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
OIDC登录按钮文字 OIDC登录按钮文字
</label> </label>
@@ -9679,10 +9759,10 @@ const RegistrationConfigComponent = ({
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
OIDC登录按钮显示的文字,"使用企业账号登录""使用SSO登录""使用OIDC登录" OIDC登录按钮显示的文字,"使用企业账号登录""使用SSO登录""使用OIDC登录"
</p> </p>
</div> </div>
{/* OIDC最低信任等级 */} {/* OIDC最低信任等级 */}
<div> <div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'> <label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label> </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' 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'> <p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
LinuxDo网站有效01-4 LinuxDo网站有效01-4
</p> </p>
</div>
</div> </div>
</div> </details>
{/* 操作按钮 */} {/* 操作按钮 */}
<div className='flex justify-end'> <div className='flex justify-end'>
+8
View File
@@ -58,6 +58,8 @@ export async function POST(request: NextRequest) {
CustomAdFilterCode, CustomAdFilterCode,
CustomAdFilterVersion, CustomAdFilterVersion,
EnableRegistration, EnableRegistration,
RequireRegistrationInviteCode,
RegistrationInviteCode,
RegistrationRequireTurnstile, RegistrationRequireTurnstile,
LoginRequireTurnstile, LoginRequireTurnstile,
TurnstileSiteKey, TurnstileSiteKey,
@@ -103,6 +105,8 @@ export async function POST(request: NextRequest) {
CustomAdFilterCode?: string; CustomAdFilterCode?: string;
CustomAdFilterVersion?: number; CustomAdFilterVersion?: number;
EnableRegistration?: boolean; EnableRegistration?: boolean;
RequireRegistrationInviteCode?: boolean;
RegistrationInviteCode?: string;
RegistrationRequireTurnstile?: boolean; RegistrationRequireTurnstile?: boolean;
LoginRequireTurnstile?: boolean; LoginRequireTurnstile?: boolean;
TurnstileSiteKey?: string; TurnstileSiteKey?: string;
@@ -148,6 +152,8 @@ export async function POST(request: NextRequest) {
(CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') || (CustomAdFilterCode !== undefined && typeof CustomAdFilterCode !== 'string') ||
(CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') || (CustomAdFilterVersion !== undefined && typeof CustomAdFilterVersion !== 'number') ||
(EnableRegistration !== undefined && typeof EnableRegistration !== 'boolean') || (EnableRegistration !== undefined && typeof EnableRegistration !== 'boolean') ||
(RequireRegistrationInviteCode !== undefined && typeof RequireRegistrationInviteCode !== 'boolean') ||
(RegistrationInviteCode !== undefined && typeof RegistrationInviteCode !== 'string') ||
(RegistrationRequireTurnstile !== undefined && typeof RegistrationRequireTurnstile !== 'boolean') || (RegistrationRequireTurnstile !== undefined && typeof RegistrationRequireTurnstile !== 'boolean') ||
(LoginRequireTurnstile !== undefined && typeof LoginRequireTurnstile !== 'boolean') || (LoginRequireTurnstile !== undefined && typeof LoginRequireTurnstile !== 'boolean') ||
(TurnstileSiteKey !== undefined && typeof TurnstileSiteKey !== 'string') || (TurnstileSiteKey !== undefined && typeof TurnstileSiteKey !== 'string') ||
@@ -208,6 +214,8 @@ export async function POST(request: NextRequest) {
CustomAdFilterCode, CustomAdFilterCode,
CustomAdFilterVersion, CustomAdFilterVersion,
EnableRegistration, EnableRegistration,
RequireRegistrationInviteCode,
RegistrationInviteCode,
RegistrationRequireTurnstile, RegistrationRequireTurnstile,
LoginRequireTurnstile, LoginRequireTurnstile,
TurnstileSiteKey, 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') { if (!username || typeof username !== 'string') {
@@ -69,6 +69,9 @@ export async function POST(req: NextRequest) {
if (!password || typeof password !== 'string') { if (!password || typeof password !== 'string') {
return NextResponse.json({ error: '密码不能为空' }, { status: 400 }); return NextResponse.json({ error: '密码不能为空' }, { status: 400 });
} }
if (inviteCode !== undefined && typeof inviteCode !== 'string') {
return NextResponse.json({ error: '邀请码格式错误' }, { status: 400 });
}
// 验证用户名格式(只允许字母、数字、下划线,长度3-20) // 验证用户名格式(只允许字母、数字、下划线,长度3-20)
if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { 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; let releaseLock: (() => void) | null = null;
try { try {
+1
View File
@@ -50,6 +50,7 @@ export async function GET(request: NextRequest) {
WatchRoom: watchRoomConfig, WatchRoom: watchRoomConfig,
EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true', EnableOfflineDownload: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true',
EnableRegistration: config.SiteConfig.EnableRegistration || false, EnableRegistration: config.SiteConfig.EnableRegistration || false,
RequireRegistrationInviteCode: config.SiteConfig.RequireRegistrationInviteCode || false,
RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false, RegistrationRequireTurnstile: config.SiteConfig.RegistrationRequireTurnstile || false,
LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false, LoginRequireTurnstile: config.SiteConfig.LoginRequireTurnstile || false,
TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '', TurnstileSiteKey: config.SiteConfig.TurnstileSiteKey || '',
+3
View File
@@ -75,6 +75,7 @@ export default async function RootLayout({
let progressThumbPresetId = ''; let progressThumbPresetId = '';
let progressThumbCustomUrl = ''; let progressThumbCustomUrl = '';
let enableRegistration = false; let enableRegistration = false;
let requireRegistrationInviteCode = false;
let loginRequireTurnstile = false; let loginRequireTurnstile = false;
let registrationRequireTurnstile = false; let registrationRequireTurnstile = false;
let turnstileSiteKey = ''; let turnstileSiteKey = '';
@@ -125,6 +126,7 @@ export default async function RootLayout({
progressThumbPresetId = config.ThemeConfig?.progressThumbPresetId || ''; progressThumbPresetId = config.ThemeConfig?.progressThumbPresetId || '';
progressThumbCustomUrl = config.ThemeConfig?.progressThumbCustomUrl || ''; progressThumbCustomUrl = config.ThemeConfig?.progressThumbCustomUrl || '';
enableRegistration = config.SiteConfig.EnableRegistration || false; enableRegistration = config.SiteConfig.EnableRegistration || false;
requireRegistrationInviteCode = config.SiteConfig.RequireRegistrationInviteCode || false;
loginRequireTurnstile = config.SiteConfig.LoginRequireTurnstile || false; loginRequireTurnstile = config.SiteConfig.LoginRequireTurnstile || false;
registrationRequireTurnstile = config.SiteConfig.RegistrationRequireTurnstile || false; registrationRequireTurnstile = config.SiteConfig.RegistrationRequireTurnstile || false;
turnstileSiteKey = config.SiteConfig.TurnstileSiteKey || ''; turnstileSiteKey = config.SiteConfig.TurnstileSiteKey || '';
@@ -195,6 +197,7 @@ export default async function RootLayout({
PROGRESS_THUMB_PRESET_ID: progressThumbPresetId, PROGRESS_THUMB_PRESET_ID: progressThumbPresetId,
PROGRESS_THUMB_CUSTOM_URL: progressThumbCustomUrl, PROGRESS_THUMB_CUSTOM_URL: progressThumbCustomUrl,
ENABLE_REGISTRATION: enableRegistration, ENABLE_REGISTRATION: enableRegistration,
REQUIRE_REGISTRATION_INVITE_CODE: requireRegistrationInviteCode,
LOGIN_REQUIRE_TURNSTILE: loginRequireTurnstile, LOGIN_REQUIRE_TURNSTILE: loginRequireTurnstile,
REGISTRATION_REQUIRE_TURNSTILE: registrationRequireTurnstile, REGISTRATION_REQUIRE_TURNSTILE: registrationRequireTurnstile,
TURNSTILE_SITE_KEY: turnstileSiteKey, TURNSTILE_SITE_KEY: turnstileSiteKey,
+30
View File
@@ -73,6 +73,7 @@ function RegisterPageClient() {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
@@ -108,6 +109,7 @@ function RegisterPageClient() {
// 设置站点配置 // 设置站点配置
const config = { const config = {
EnableRegistration: runtimeConfig?.ENABLE_REGISTRATION || false, EnableRegistration: runtimeConfig?.ENABLE_REGISTRATION || false,
RequireRegistrationInviteCode: runtimeConfig?.REQUIRE_REGISTRATION_INVITE_CODE || false,
RegistrationRequireTurnstile: runtimeConfig?.REGISTRATION_REQUIRE_TURNSTILE || false, RegistrationRequireTurnstile: runtimeConfig?.REGISTRATION_REQUIRE_TURNSTILE || false,
TurnstileSiteKey: runtimeConfig?.TURNSTILE_SITE_KEY || '', TurnstileSiteKey: runtimeConfig?.TURNSTILE_SITE_KEY || '',
}; };
@@ -167,6 +169,11 @@ function RegisterPageClient() {
return; return;
} }
if (siteConfig?.RequireRegistrationInviteCode && !inviteCode.trim()) {
setError('请输入邀请码');
return;
}
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError('两次输入的密码不一致'); setError('两次输入的密码不一致');
return; return;
@@ -191,6 +198,7 @@ function RegisterPageClient() {
body: JSON.stringify({ body: JSON.stringify({
username, username,
password, password,
inviteCode: siteConfig?.RequireRegistrationInviteCode ? inviteCode.trim() : undefined,
turnstileToken: siteConfig?.RegistrationRequireTurnstile ? turnstileToken : undefined, turnstileToken: siteConfig?.RegistrationRequireTurnstile ? turnstileToken : undefined,
}), }),
}); });
@@ -340,6 +348,27 @@ function RegisterPageClient() {
</div> </div>
</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 */} {/* Cloudflare Turnstile */}
{siteConfig?.RegistrationRequireTurnstile && siteConfig?.TurnstileSiteKey && ( {siteConfig?.RegistrationRequireTurnstile && siteConfig?.TurnstileSiteKey && (
<div id='turnstile-container' className='flex justify-center'></div> <div id='turnstile-container' className='flex justify-center'></div>
@@ -354,6 +383,7 @@ function RegisterPageClient() {
type='submit' type='submit'
disabled={ disabled={
!username || !password || !confirmPassword || loading || !username || !password || !confirmPassword || loading ||
(siteConfig?.RequireRegistrationInviteCode && !inviteCode.trim()) ||
(siteConfig?.RegistrationRequireTurnstile && !turnstileToken) (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' 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; // 代码版本号(时间戳) CustomAdFilterVersion?: number; // 代码版本号(时间戳)
// 注册相关配置 // 注册相关配置
EnableRegistration?: boolean; // 开启注册 EnableRegistration?: boolean; // 开启注册
RequireRegistrationInviteCode?: boolean; // 注册时要求邀请码
RegistrationInviteCode?: string; // 通用注册邀请码
RegistrationRequireTurnstile?: boolean; // 注册启用Cloudflare Turnstile RegistrationRequireTurnstile?: boolean; // 注册启用Cloudflare Turnstile
LoginRequireTurnstile?: boolean; // 登录启用Cloudflare Turnstile LoginRequireTurnstile?: boolean; // 登录启用Cloudflare Turnstile
TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key TurnstileSiteKey?: string; // Cloudflare Turnstile Site Key
+40
View File
@@ -263,6 +263,14 @@ async function getInitConfig(configFile: string, subConfig: {
MagnetAcgripReverseProxy: '', MagnetAcgripReverseProxy: '',
// 评论功能开关 // 评论功能开关
EnableComments: false, EnableComments: false,
EnableRegistration: false,
RequireRegistrationInviteCode: false,
RegistrationInviteCode: '',
RegistrationRequireTurnstile: false,
LoginRequireTurnstile: false,
TurnstileSiteKey: '',
TurnstileSecretKey: '',
DefaultUserTags: [],
}, },
UserConfig: { UserConfig: {
Users: [], Users: [],
@@ -442,6 +450,14 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
MagnetDmhyReverseProxy: '', MagnetDmhyReverseProxy: '',
MagnetAcgripReverseProxy: '', MagnetAcgripReverseProxy: '',
EnableComments: false, 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) { if (adminConfig.SiteConfig.EnableComments === undefined) {
adminConfig.SiteConfig.EnableComments = false; 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) { if (adminConfig.SiteConfig.PansouKeywordBlocklist === undefined) {
adminConfig.SiteConfig.PansouKeywordBlocklist = ''; adminConfig.SiteConfig.PansouKeywordBlocklist = '';
} }