implement captcha to register and login

This commit is contained in:
2026-02-03 16:19:45 +01:00
parent 3669321139
commit b5e94e73c5
3 changed files with 127 additions and 68 deletions

View File

@@ -41,8 +41,9 @@
- Proof-of-Work basiert, kein Tracking - Proof-of-Work basiert, kein Tracking
- Keine externe Abhängigkeit, keine Lizenzkosten - Keine externe Abhängigkeit, keine Lizenzkosten
- Client löst SHA256-Challenge (Difficulty 4, ~1-3 Sek) - Client löst SHA256-Challenge (Difficulty 4, ~1-3 Sek)
- Bei Account-Erstellung - Bei Account-Erstellung (immer)
- Bei Anzeigen-Erstellung - Bei Anzeigen-Erstellung (immer)
- Bei Login (nur nach 3+ Fehlversuchen)
- Implementierung: `js/services/pow-captcha.js`, `js/components/pow-captcha.js` - Implementierung: `js/services/pow-captcha.js`, `js/components/pow-captcha.js`
## Payment-Integration ## Payment-Integration

View File

@@ -4,6 +4,7 @@
import { t, i18n } from '../i18n.js' import { t, i18n } from '../i18n.js'
import { auth } from '../services/auth.js' import { auth } from '../services/auth.js'
import './pow-captcha.js'
class AuthModal extends HTMLElement { class AuthModal extends HTMLElement {
constructor() { constructor() {
@@ -12,6 +13,7 @@ class AuthModal extends HTMLElement {
this.generatedUuid = null this.generatedUuid = null
this.error = null this.error = null
this.loading = false this.loading = false
this.loginAttempts = 0
} }
connectedCallback() { connectedCallback() {
@@ -94,6 +96,12 @@ class AuthModal extends HTMLElement {
> >
</div> </div>
${this.loginAttempts >= 3 ? `
<div class="form-group">
<pow-captcha id="login-captcha"></pow-captcha>
</div>
` : ''}
<button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}> <button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}>
${this.loading ? t('auth.loggingIn') : t('auth.login')} ${this.loading ? t('auth.loggingIn') : t('auth.login')}
</button> </button>
@@ -117,6 +125,10 @@ class AuthModal extends HTMLElement {
${this.error ? `<div class="auth-error">${this.error}</div>` : ''} ${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
<div class="form-group">
<pow-captcha id="register-captcha"></pow-captcha>
</div>
<button <button
class="btn btn-primary btn-lg btn-block" class="btn btn-primary btn-lg btn-block"
id="generate-uuid" id="generate-uuid"
@@ -210,6 +222,16 @@ class AuthModal extends HTMLElement {
return return
} }
// Check captcha after 3 failed attempts
if (this.loginAttempts >= 3) {
const captcha = this.querySelector('#login-captcha')
if (!captcha?.isSolved()) {
this.error = t('captcha.error')
this.render()
return
}
}
this.loading = true this.loading = true
this.error = null this.error = null
this.render() this.render()
@@ -219,18 +241,34 @@ class AuthModal extends HTMLElement {
this.loading = false this.loading = false
if (result.success) { if (result.success) {
this.loginAttempts = 0
this.hide() this.hide()
this.dispatchEvent(new CustomEvent('login', { detail: { success: true } })) this.dispatchEvent(new CustomEvent('login', { detail: { success: true } }))
} else { } else {
this.loginAttempts++
this.error = result.error || t('auth.invalidUuid') this.error = result.error || t('auth.invalidUuid')
this.render() this.render()
} }
} }
async handleRegister() { async handleRegister() {
// Require captcha for registration
const captcha = this.querySelector('#register-captcha')
if (!captcha?.isSolved()) {
this.error = t('captcha.error')
this.render()
return
}
this.loading = true this.loading = true
this.error = null this.error = null
this.render()
// Update button state without full re-render (keeps captcha state)
const btn = this.querySelector('#generate-uuid')
if (btn) {
btn.disabled = true
btn.textContent = t('auth.creating')
}
const result = await auth.createAccount() const result = await auth.createAccount()

View File

@@ -6,6 +6,7 @@ export class PowCaptcha extends HTMLElement {
constructor() { constructor() {
super() super()
this.solved = false this.solved = false
this.solving = false
this.solution = null this.solution = null
this.unsubscribe = null this.unsubscribe = null
} }
@@ -21,46 +22,42 @@ export class PowCaptcha extends HTMLElement {
render() { render() {
this.innerHTML = ` this.innerHTML = `
<div class="pow-captcha"> <div class="pow-captcha ${this.solved ? 'pow-captcha--solved' : ''} ${this.solving ? 'pow-captcha--solving' : ''}">
<div class="pow-captcha-status"> <label class="pow-captcha-label">
${this.solved <span class="pow-captcha-checkbox">
? `<span class="pow-captcha-success">✓ ${t('captcha.verified')}</span>` ${this.solved
: `<button class="pow-captcha-btn" type="button">${t('captcha.verify')}</button>` ? `<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>
</div> </svg>`
<div class="pow-captcha-progress" style="display: none;"> : this.solving
<div class="pow-captcha-progress-bar"></div> ? `<span class="pow-captcha-spinner"></span>`
<span class="pow-captcha-progress-text"></span> : ``
</div> }
</span>
<span class="pow-captcha-text">
${this.solved ? t('captcha.verified') : t('captcha.verify')}
</span>
</label>
</div> </div>
` `
if (!this.solved) { if (!this.solved && !this.solving) {
this.querySelector('.pow-captcha-btn').addEventListener('click', () => this.solve()) this.querySelector('.pow-captcha-label').addEventListener('click', () => this.solve())
} }
} }
async solve() { async solve() {
const btn = this.querySelector('.pow-captcha-btn') if (this.solving || this.solved) return
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 this.solving = true
btn.textContent = t('captcha.solving') this.render()
progress.style.display = 'flex'
try { try {
// Generate challenge (in production, fetch from server) // Generate challenge (in production, fetch from server)
const { challenge, difficulty, timestamp, signature } = generateChallenge() const { challenge, difficulty, timestamp, signature } = generateChallenge()
// Solve with progress updates // Solve challenge
const result = await solveChallenge(challenge, difficulty, ({ attempts, elapsed }) => { const result = await solveChallenge(challenge, difficulty)
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 // Store solution for form submission
this.solution = { this.solution = {
@@ -70,13 +67,14 @@ export class PowCaptcha extends HTMLElement {
timestamp timestamp
} }
this.solving = false
this.solved = true this.solved = true
this.dispatchEvent(new CustomEvent('solved', { detail: this.solution })) this.dispatchEvent(new CustomEvent('solved', { detail: this.solution }))
this.render() this.render()
} catch (error) { } catch (error) {
btn.disabled = false this.solving = false
btn.textContent = t('captcha.error') this.render()
console.error('PoW Captcha error:', error) console.error('PoW Captcha error:', error)
} }
} }
@@ -105,52 +103,74 @@ customElements.define('pow-captcha', PowCaptcha)
const style = document.createElement('style') const style = document.createElement('style')
style.textContent = ` style.textContent = `
.pow-captcha { .pow-captcha {
padding: var(--space-md); display: inline-block;
padding: var(--space-md) var(--space-lg);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: var(--color-surface); background: var(--color-surface, var(--color-bg-secondary));
} }
.pow-captcha-btn { .pow-captcha-label {
padding: var(--space-sm) var(--space-md); display: flex;
background: var(--color-primary); align-items: center;
color: var(--color-bg); gap: var(--space-md);
border: none;
border-radius: var(--radius-sm);
cursor: pointer; 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); font-size: var(--font-size-sm);
color: var(--color-text);
} }
.pow-captcha-btn:disabled { .pow-captcha--solved .pow-captcha-text {
opacity: 0.6;
cursor: wait;
}
.pow-captcha-success {
color: var(--color-success, #22c55e); color: var(--color-success, #22c55e);
font-weight: 500; 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) document.head.appendChild(style)