Files
kashilo/js/components/pow-captcha.js

177 lines
4.2 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.solving = 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 ${this.solved ? 'pow-captcha--solved' : ''} ${this.solving ? 'pow-captcha--solving' : ''}">
<label class="pow-captcha-label">
<span class="pow-captcha-checkbox">
${this.solved
? `<svg class="pow-captcha-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>`
: this.solving
? `<span class="pow-captcha-spinner"></span>`
: ``
}
</span>
<span class="pow-captcha-text">
${this.solved ? t('captcha.verified') : t('captcha.verify')}
</span>
</label>
</div>
`
if (!this.solved && !this.solving) {
this.querySelector('.pow-captcha-label').addEventListener('click', () => this.solve())
}
}
async solve() {
if (this.solving || this.solved) return
this.solving = true
this.render()
try {
// Generate challenge (in production, fetch from server)
const { challenge, difficulty, timestamp, signature } = generateChallenge()
// Solve challenge
const result = await solveChallenge(challenge, difficulty)
// Store solution for form submission
this.solution = {
challenge,
nonce: result.nonce,
signature,
timestamp
}
this.solving = false
this.solved = true
this.dispatchEvent(new CustomEvent('solved', { detail: this.solution }))
this.render()
} catch (error) {
this.solving = false
this.render()
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 {
display: inline-block;
padding: var(--space-md) var(--space-lg);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface, var(--color-bg-secondary));
}
.pow-captcha-label {
display: flex;
align-items: center;
gap: var(--space-md);
cursor: pointer;
user-select: none;
}
.pow-captcha--solving .pow-captcha-label,
.pow-captcha--solved .pow-captcha-label {
cursor: default;
}
.pow-captcha-checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--color-border);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg);
transition: all 0.2s ease;
}
.pow-captcha:hover:not(.pow-captcha--solved):not(.pow-captcha--solving) .pow-captcha-checkbox {
border-color: var(--color-primary);
}
.pow-captcha--solved .pow-captcha-checkbox {
background: var(--color-success, #22c55e);
border-color: var(--color-success, #22c55e);
}
.pow-captcha-check {
width: 16px;
height: 16px;
color: white;
}
.pow-captcha-spinner {
width: 16px;
height: 16px;
border: 2px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: pow-spin 0.8s linear infinite;
}
@keyframes pow-spin {
to { transform: rotate(360deg); }
}
.pow-captcha-text {
font-size: var(--font-size-sm);
color: var(--color-text);
}
.pow-captcha--solved .pow-captcha-text {
color: var(--color-success, #22c55e);
font-weight: 500;
}
`
document.head.appendChild(style)