From b5e94e73c528dc29db402f72aeb825c66b0356af Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Tue, 3 Feb 2026 16:19:45 +0100 Subject: [PATCH] implement captcha to register and login --- docs/MONETIZATION.md | 5 +- js/components/auth-modal.js | 40 +++++++++- js/components/pow-captcha.js | 150 ++++++++++++++++++++--------------- 3 files changed, 127 insertions(+), 68 deletions(-) diff --git a/docs/MONETIZATION.md b/docs/MONETIZATION.md index 75e893c..5e32a3b 100644 --- a/docs/MONETIZATION.md +++ b/docs/MONETIZATION.md @@ -41,8 +41,9 @@ - Proof-of-Work basiert, kein Tracking - Keine externe Abhängigkeit, keine Lizenzkosten - Client löst SHA256-Challenge (Difficulty 4, ~1-3 Sek) -- Bei Account-Erstellung -- Bei Anzeigen-Erstellung +- Bei Account-Erstellung (immer) +- Bei Anzeigen-Erstellung (immer) +- Bei Login (nur nach 3+ Fehlversuchen) - Implementierung: `js/services/pow-captcha.js`, `js/components/pow-captcha.js` ## Payment-Integration diff --git a/js/components/auth-modal.js b/js/components/auth-modal.js index 297b765..70ae744 100644 --- a/js/components/auth-modal.js +++ b/js/components/auth-modal.js @@ -4,6 +4,7 @@ import { t, i18n } from '../i18n.js' import { auth } from '../services/auth.js' +import './pow-captcha.js' class AuthModal extends HTMLElement { constructor() { @@ -12,6 +13,7 @@ class AuthModal extends HTMLElement { this.generatedUuid = null this.error = null this.loading = false + this.loginAttempts = 0 } connectedCallback() { @@ -94,6 +96,12 @@ class AuthModal extends HTMLElement { > + ${this.loginAttempts >= 3 ? ` +
+ +
+ ` : ''} + @@ -117,6 +125,10 @@ class AuthModal extends HTMLElement { ${this.error ? `
${this.error}
` : ''} +
+ +
+ ` - } - - +
+
` - if (!this.solved) { - this.querySelector('.pow-captcha-btn').addEventListener('click', () => this.solve()) + if (!this.solved && !this.solving) { + this.querySelector('.pow-captcha-label').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') + if (this.solving || this.solved) return - btn.disabled = true - btn.textContent = t('captcha.solving') - progress.style.display = 'flex' + this.solving = true + this.render() 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')}` - }) + // Solve challenge + const result = await solveChallenge(challenge, difficulty) // Store solution for form submission this.solution = { @@ -70,13 +67,14 @@ export class PowCaptcha extends HTMLElement { timestamp } + this.solving = false this.solved = true this.dispatchEvent(new CustomEvent('solved', { detail: this.solution })) this.render() } catch (error) { - btn.disabled = false - btn.textContent = t('captcha.error') + this.solving = false + this.render() console.error('PoW Captcha error:', error) } } @@ -105,52 +103,74 @@ customElements.define('pow-captcha', PowCaptcha) const style = document.createElement('style') style.textContent = ` .pow-captcha { - padding: var(--space-md); + 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); + background: var(--color-surface, var(--color-bg-secondary)); } - .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); + .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-btn:disabled { - opacity: 0.6; - cursor: wait; - } - - .pow-captcha-success { + .pow-captcha--solved .pow-captcha-text { 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)