fix:修复Turnstile真人验证功能
This commit is contained in:
1 parent
e9b1b1827c
commit
cea265a1dd
6 files changed
+108
-35
No files matched your search
@@ -197,9 +197,18 @@ func (s *ConfigService) SetOption(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
data := do.SysOptions{Value: value}
|
||||
// Sync metadata from registry so that changes to IsPublic/Category/Description
|
||||
// are reflected in the DB row on the next write (e.g. a setting newly marked
|
||||
// as IsPublic will become visible to GetPublicOptions).
|
||||
if def := GetSettingDef(key); def != nil {
|
||||
data.Category = def.Category
|
||||
data.Description = def.Label
|
||||
data.IsPublic = def.IsPublic
|
||||
}
|
||||
_, err = dao.SysOptions.Ctx(ctx).
|
||||
Where("key", key).
|
||||
Data(do.SysOptions{Value: value}).
|
||||
Data(data).
|
||||
Update()
|
||||
} else {
|
||||
def := GetSettingDef(key)
|
||||
|
||||
@@ -120,9 +120,9 @@ var Registry = []SettingDef{
|
||||
{Key: "password_min_length", Type: SettingTypeInt, Default: "8", Category: "security",
|
||||
Label: "密码最小长度", Validation: "min:6,max:32"},
|
||||
{Key: "turnstile_enabled", Type: SettingTypeBool, Default: "false", Category: "security",
|
||||
Label: "启用 Turnstile 人机验证"},
|
||||
Label: "启用 Turnstile 人机验证", IsPublic: true},
|
||||
{Key: "turnstile_site_key", Type: SettingTypeString, Default: "", Category: "security",
|
||||
Label: "Turnstile Site Key", Sensitive: false},
|
||||
Label: "Turnstile Site Key", Sensitive: false, IsPublic: true},
|
||||
{Key: "turnstile_secret_key", Type: SettingTypeString, Default: "", Category: "security",
|
||||
Label: "Turnstile Secret Key", Sensitive: true},
|
||||
{Key: "captcha_expire_seconds", Type: SettingTypeInt, Default: "300", Category: "security",
|
||||
|
||||
@@ -70,14 +70,16 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1.
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 人机验证:优先 Turnstile,否则滑块验证
|
||||
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Turnstile 未启用时,使用滑块验证
|
||||
if err := common.CheckCaptchaRequired(ctx, "tenant_register", req.CaptchaKey, req.CaptchaX); err != nil {
|
||||
return nil, err
|
||||
// 人机验证:Turnstile 启用时只验 Turnstile,否则用滑块验证
|
||||
turnstileEnabled := common.Config().GetBool(ctx, "turnstile_enabled")
|
||||
if turnstileEnabled {
|
||||
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := common.CheckCaptchaRequired(ctx, "tenant_register", req.CaptchaKey, req.CaptchaX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,13 +242,16 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1.
|
||||
func (s *sTenant) Login(ctx context.Context, req *v1.TenantLoginReq) (*v1.TenantLoginRes, error) {
|
||||
account := strings.TrimSpace(req.Account)
|
||||
|
||||
// Check captcha if required
|
||||
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := common.CheckCaptchaRequired(ctx, "tenant_login", req.CaptchaKey, req.CaptchaX); err != nil {
|
||||
return nil, err
|
||||
// Check captcha: Turnstile takes priority; slider captcha is fallback
|
||||
turnstileEnabled := common.Config().GetBool(ctx, "turnstile_enabled")
|
||||
if turnstileEnabled {
|
||||
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := common.CheckCaptchaRequired(ctx, "tenant_login", req.CaptchaKey, req.CaptchaX); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Get client info early for login history recording
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- +goose Up
|
||||
-- Sync is_public flag from code registry to DB rows.
|
||||
-- Settings like turnstile_enabled/turnstile_site_key were marked IsPublic in the
|
||||
-- registry but the DB rows still had is_public=false, causing GetPublicOptions
|
||||
-- (WHERE is_public=true) to skip them and return stale defaults to the tenant frontend.
|
||||
|
||||
UPDATE sys_options SET is_public = true, updated_at = NOW()
|
||||
WHERE key IN (
|
||||
'site_name',
|
||||
'site_description',
|
||||
'register_enabled',
|
||||
'register_email_verification',
|
||||
'maintenance_message',
|
||||
'maintenance_duration',
|
||||
'api_maintenance_enabled',
|
||||
'oauth_auto_register',
|
||||
'oauth_github_enabled',
|
||||
'oauth_google_enabled',
|
||||
'turnstile_enabled',
|
||||
'turnstile_site_key',
|
||||
'agreement_enabled'
|
||||
) AND is_public = false;
|
||||
|
||||
-- +goose Down
|
||||
-- No-op: reverting is_public to false would break public settings again,
|
||||
-- so we leave the corrected values in place.
|
||||
SELECT 1;
|
||||
@@ -114,13 +114,14 @@ export const useTenantAuthStore = defineStore('tenant-auth', () => {
|
||||
persist()
|
||||
}
|
||||
|
||||
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number }): Promise<any> {
|
||||
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number; turnstileToken?: string }): Promise<any> {
|
||||
const { data } = await request.post('/tenant/auth/login', {
|
||||
account,
|
||||
password,
|
||||
type,
|
||||
captcha_key: captcha?.captchaKey,
|
||||
captcha_x: captcha?.captchaX,
|
||||
turnstile_token: captcha?.turnstileToken,
|
||||
})
|
||||
if (data.data?.totp_required) {
|
||||
return data.data
|
||||
|
||||
@@ -36,10 +36,13 @@ const provisionalToken = ref('')
|
||||
const totpCode = ref('')
|
||||
const totpLoading = ref(false)
|
||||
|
||||
// Captcha state — always required
|
||||
// Captcha state
|
||||
const captcha = reactive({ captchaKey: '', captchaX: 0 })
|
||||
const turnstileToken = ref('')
|
||||
const turnstileToken = ref('')
|
||||
const captchaRef = ref<InstanceType<typeof SlideCaptcha> | null>(null)
|
||||
const turnstileRef = ref<InstanceType<typeof Turnstile> | null>(null)
|
||||
|
||||
const useTurnstile = computed(() => settings.value['turnstile_enabled'] === true)
|
||||
|
||||
const emailInput = ref<HTMLInputElement | null>(null)
|
||||
const accountInput = ref<HTMLInputElement | null>(null)
|
||||
@@ -124,15 +127,24 @@ function validateRam(): boolean {
|
||||
|
||||
async function handleAdminLogin() {
|
||||
if (!validateAdmin()) return
|
||||
if (!captcha.captchaKey || !captcha.captchaX) {
|
||||
errorMsg.value = '请先完成滑块验证'
|
||||
return
|
||||
if (useTurnstile.value) {
|
||||
if (!turnstileToken.value) {
|
||||
errorMsg.value = '请先完成人机验证'
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!captcha.captchaKey || !captcha.captchaX) {
|
||||
errorMsg.value = '请先完成滑块验证'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
try {
|
||||
const captchaPayload = { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
|
||||
const captchaPayload = useTurnstile.value
|
||||
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
|
||||
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
|
||||
const res = await authStore.login(adminForm.email, adminForm.password, 'admin', captchaPayload)
|
||||
if (res?.totp_required) {
|
||||
provisionalToken.value = res.provisional_token
|
||||
@@ -142,12 +154,16 @@ async function handleAdminLogin() {
|
||||
proceedAfterLogin()
|
||||
} catch (err) {
|
||||
const apiErr = extractApiError(err)
|
||||
if (apiErr?.code === 10058) {
|
||||
if (useTurnstile.value) {
|
||||
turnstileRef.value?.reset()
|
||||
errorMsg.value = apiErr?.message || '登录失败,请检查邮箱和密码'
|
||||
} else if (apiErr?.code === 10058) {
|
||||
errorMsg.value = '滑块验证失败,请重新拖动'
|
||||
captchaRef.value?.resetCaptcha()
|
||||
} else {
|
||||
errorMsg.value = apiErr?.message || '登录失败,请检查邮箱和密码'
|
||||
captchaRef.value?.resetCaptcha()
|
||||
}
|
||||
captchaRef.value?.resetCaptcha()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -155,15 +171,24 @@ async function handleAdminLogin() {
|
||||
|
||||
async function handleRamLogin() {
|
||||
if (!validateRam()) return
|
||||
if (!captcha.captchaKey || !captcha.captchaX) {
|
||||
errorMsg.value = '请先完成滑块验证'
|
||||
return
|
||||
if (useTurnstile.value) {
|
||||
if (!turnstileToken.value) {
|
||||
errorMsg.value = '请先完成人机验证'
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!captcha.captchaKey || !captcha.captchaX) {
|
||||
errorMsg.value = '请先完成滑块验证'
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
try {
|
||||
const captchaPayload = { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
|
||||
const captchaPayload = useTurnstile.value
|
||||
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
|
||||
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
|
||||
const res = await authStore.login(ramForm.account, ramForm.password, 'ram', captchaPayload)
|
||||
if (res?.totp_required) {
|
||||
provisionalToken.value = res.provisional_token
|
||||
@@ -173,12 +198,16 @@ async function handleRamLogin() {
|
||||
proceedAfterLogin()
|
||||
} catch (err) {
|
||||
const apiErr = extractApiError(err)
|
||||
if (apiErr?.code === 10058) {
|
||||
if (useTurnstile.value) {
|
||||
turnstileRef.value?.reset()
|
||||
errorMsg.value = apiErr?.message || '登录失败,请检查用户名和密码'
|
||||
} else if (apiErr?.code === 10058) {
|
||||
errorMsg.value = '滑块验证失败,请重新拖动'
|
||||
captchaRef.value?.resetCaptcha()
|
||||
} else {
|
||||
errorMsg.value = apiErr?.message || '登录失败,请检查用户名和密码'
|
||||
captchaRef.value?.resetCaptcha()
|
||||
}
|
||||
captchaRef.value?.resetCaptcha()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -335,7 +364,8 @@ async function handleOAuthLogin(provider: string) {
|
||||
</div>
|
||||
|
||||
<!-- Captcha -->
|
||||
<SlideCaptcha ref="captchaRef" v-model="captcha" />
|
||||
<Turnstile v-if="useTurnstile" ref="turnstileRef" v-model="turnstileToken" :site-key="settings['turnstile_site_key']" />
|
||||
<SlideCaptcha v-else ref="captchaRef" v-model="captcha" />
|
||||
|
||||
<!-- Error Message -->
|
||||
<transition name="slide-fade">
|
||||
@@ -432,7 +462,8 @@ async function handleOAuthLogin(provider: string) {
|
||||
</div>
|
||||
|
||||
<!-- Captcha -->
|
||||
<SlideCaptcha ref="captchaRef" v-model="captcha" />
|
||||
<Turnstile v-if="useTurnstile" ref="turnstileRef" v-model="turnstileToken" :site-key="settings['turnstile_site_key']" />
|
||||
<SlideCaptcha v-else ref="captchaRef" v-model="captcha" />
|
||||
|
||||
<!-- Error Message -->
|
||||
<transition name="slide-fade">
|
||||
|
||||
Reference in new issue
Block a user