Files
kashilo/js/components/pow-captcha.js
2026-02-03 14:44:36 +01:00

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)