157 lines
3.9 KiB
JavaScript
157 lines
3.9 KiB
JavaScript
// PoW Captcha Web Component
|
|
import { generateChallenge, solveChallenge } from '../services/pow-captcha.js'
|
|
import { t, i18n } from '../i18n.js'
|
|
|
|
export class PowCaptcha extends HTMLElement {
|
|
constructor() {
|
|
super()
|
|
this.solved = false
|
|
this.solution = null
|
|
this.unsubscribe = null
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render()
|
|
this.unsubscribe = i18n.subscribe(() => this.render())
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.unsubscribe) this.unsubscribe()
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<div class="pow-captcha">
|
|
<div class="pow-captcha-status">
|
|
${this.solved
|
|
? `<span class="pow-captcha-success">✓ ${t('captcha.verified')}</span>`
|
|
: `<button class="pow-captcha-btn" type="button">${t('captcha.verify')}</button>`
|
|
}
|
|
</div>
|
|
<div class="pow-captcha-progress" style="display: none;">
|
|
<div class="pow-captcha-progress-bar"></div>
|
|
<span class="pow-captcha-progress-text"></span>
|
|
</div>
|
|
</div>
|
|
`
|
|
|
|
if (!this.solved) {
|
|
this.querySelector('.pow-captcha-btn').addEventListener('click', () => this.solve())
|
|
}
|
|
}
|
|
|
|
async solve() {
|
|
const btn = this.querySelector('.pow-captcha-btn')
|
|
const progress = this.querySelector('.pow-captcha-progress')
|
|
const progressBar = this.querySelector('.pow-captcha-progress-bar')
|
|
const progressText = this.querySelector('.pow-captcha-progress-text')
|
|
|
|
btn.disabled = true
|
|
btn.textContent = t('captcha.solving')
|
|
progress.style.display = 'flex'
|
|
|
|
try {
|
|
// Generate challenge (in production, fetch from server)
|
|
const { challenge, difficulty, timestamp, signature } = generateChallenge()
|
|
|
|
// Solve with progress updates
|
|
const result = await solveChallenge(challenge, difficulty, ({ attempts, elapsed }) => {
|
|
const estimatedTotal = Math.pow(16, difficulty) / 2
|
|
const percent = Math.min((attempts / estimatedTotal) * 100, 95)
|
|
progressBar.style.width = `${percent}%`
|
|
progressText.textContent = `${attempts.toLocaleString()} ${t('captcha.attempts')}`
|
|
})
|
|
|
|
// Store solution for form submission
|
|
this.solution = {
|
|
challenge,
|
|
nonce: result.nonce,
|
|
signature,
|
|
timestamp
|
|
}
|
|
|
|
this.solved = true
|
|
this.dispatchEvent(new CustomEvent('solved', { detail: this.solution }))
|
|
this.render()
|
|
|
|
} catch (error) {
|
|
btn.disabled = false
|
|
btn.textContent = t('captcha.error')
|
|
console.error('PoW Captcha error:', error)
|
|
}
|
|
}
|
|
|
|
// Get solution for form submission
|
|
getSolution() {
|
|
return this.solution
|
|
}
|
|
|
|
// Check if solved
|
|
isSolved() {
|
|
return this.solved
|
|
}
|
|
|
|
// Reset captcha
|
|
reset() {
|
|
this.solved = false
|
|
this.solution = null
|
|
this.render()
|
|
}
|
|
}
|
|
|
|
customElements.define('pow-captcha', PowCaptcha)
|
|
|
|
// Add styles
|
|
const style = document.createElement('style')
|
|
style.textContent = `
|
|
.pow-captcha {
|
|
padding: var(--space-md);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-surface);
|
|
}
|
|
|
|
.pow-captcha-btn {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--color-primary);
|
|
color: var(--color-bg);
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.pow-captcha-btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: wait;
|
|
}
|
|
|
|
.pow-captcha-success {
|
|
color: var(--color-success, #22c55e);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pow-captcha-progress {
|
|
margin-top: var(--space-sm);
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.pow-captcha-progress-bar {
|
|
height: 4px;
|
|
background: var(--color-primary);
|
|
border-radius: 2px;
|
|
width: 0%;
|
|
transition: width 0.1s;
|
|
flex: 1;
|
|
}
|
|
|
|
.pow-captcha-progress-text {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--color-text-muted);
|
|
min-width: 100px;
|
|
text-align: right;
|
|
}
|
|
`
|
|
document.head.appendChild(style)
|