179 lines
4.3 KiB
JavaScript
179 lines
4.3 KiB
JavaScript
// PoW Captcha Web Component
|
|
import { generateChallenge, solveChallenge, verifySolution } 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 {
|
|
const { challenge, difficulty, timestamp, signature } = await generateChallenge()
|
|
|
|
const result = await solveChallenge(challenge, difficulty)
|
|
|
|
const solution = {
|
|
challenge,
|
|
difficulty,
|
|
nonce: result.nonce,
|
|
signature,
|
|
timestamp
|
|
}
|
|
|
|
const verification = await verifySolution(solution)
|
|
if (!verification.ok) throw new Error('Verification failed')
|
|
|
|
this.solution = { ...solution, token: verification.token }
|
|
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)
|