feat: improve authentication and user management
- Fix token missing issue after email code registration/login - Add last login time update for code-based login - Support email login in addition to username login - Fix password login for code-registered users (allow setting password) - Fix referral code parameter passing from URL hash - Add email column to user management table - Improve profile page layout (align card heights, reorganize layout) - Add i18n support for Register Bonus, Referral Bonus, Code Lock Minutes, Code Max Attempts - Fix billing service add_credits to support reference_id parameter - Update password handling documentation
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="turnstile-container" v-if="enabled">
|
||||
<div ref="turnstileRef" :id="containerId"></div>
|
||||
<div v-if="error" class="turnstile-error">
|
||||
{{ error }}
|
||||
<a @click="reset">{{ $t('user.security.retry') || 'Retry' }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
let turnstileScriptLoaded = false
|
||||
let turnstileScriptLoading = false
|
||||
const turnstileCallbacks = []
|
||||
|
||||
function loadTurnstileScript () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (turnstileScriptLoaded) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
turnstileCallbacks.push({ resolve, reject })
|
||||
|
||||
if (turnstileScriptLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
turnstileScriptLoading = true
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
|
||||
script.async = true
|
||||
script.defer = true
|
||||
|
||||
script.onload = () => {
|
||||
turnstileScriptLoaded = true
|
||||
turnstileCallbacks.forEach(cb => cb.resolve())
|
||||
turnstileCallbacks.length = 0
|
||||
}
|
||||
|
||||
script.onerror = () => {
|
||||
turnstileScriptLoading = false
|
||||
turnstileCallbacks.forEach(cb => cb.reject(new Error('Failed to load Turnstile script')))
|
||||
turnstileCallbacks.length = 0
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Turnstile',
|
||||
|
||||
props: {
|
||||
siteKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'auto' // 'light', 'dark', 'auto'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'normal' // 'normal', 'compact'
|
||||
}
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
widgetId: null,
|
||||
token: null,
|
||||
error: null,
|
||||
containerId: `turnstile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if (this.enabled && this.siteKey) {
|
||||
this.initTurnstile()
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.cleanup()
|
||||
},
|
||||
|
||||
watch: {
|
||||
siteKey (newVal) {
|
||||
if (newVal && this.enabled) {
|
||||
this.initTurnstile()
|
||||
}
|
||||
},
|
||||
enabled (newVal) {
|
||||
if (newVal && this.siteKey) {
|
||||
this.initTurnstile()
|
||||
} else {
|
||||
this.cleanup()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initTurnstile () {
|
||||
try {
|
||||
await loadTurnstileScript()
|
||||
this.renderWidget()
|
||||
} catch (e) {
|
||||
this.error = 'Failed to load verification'
|
||||
console.error('Turnstile init error:', e)
|
||||
}
|
||||
},
|
||||
|
||||
renderWidget () {
|
||||
if (!window.turnstile || !this.$refs.turnstileRef) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up existing widget
|
||||
this.cleanup()
|
||||
|
||||
this.widgetId = window.turnstile.render(this.$refs.turnstileRef, {
|
||||
sitekey: this.siteKey,
|
||||
theme: this.theme,
|
||||
size: this.size,
|
||||
callback: (token) => {
|
||||
this.token = token
|
||||
this.error = null
|
||||
this.$emit('success', token)
|
||||
},
|
||||
'error-callback': () => {
|
||||
this.token = null
|
||||
this.error = 'Verification failed'
|
||||
this.$emit('error')
|
||||
},
|
||||
'expired-callback': () => {
|
||||
this.token = null
|
||||
this.$emit('expired')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.token = null
|
||||
this.error = null
|
||||
if (window.turnstile && this.widgetId !== null) {
|
||||
window.turnstile.reset(this.widgetId)
|
||||
} else {
|
||||
this.renderWidget()
|
||||
}
|
||||
},
|
||||
|
||||
getToken () {
|
||||
return this.token
|
||||
},
|
||||
|
||||
cleanup () {
|
||||
if (window.turnstile && this.widgetId !== null) {
|
||||
try {
|
||||
window.turnstile.remove(this.widgetId)
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
this.widgetId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.turnstile-container {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.turnstile-error {
|
||||
margin-top: 8px;
|
||||
color: #ff4d4f;
|
||||
font-size: 13px;
|
||||
|
||||
a {
|
||||
margin-left: 8px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user