implement captcha to register and login
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
>
|
||||
</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' : ''}>
|
||||
${this.loading ? t('auth.loggingIn') : t('auth.login')}
|
||||
</button>
|
||||
@@ -117,6 +125,10 @@ class AuthModal extends HTMLElement {
|
||||
|
||||
${this.error ? `<div class="auth-error">${this.error}</div>` : ''}
|
||||
|
||||
<div class="form-group">
|
||||
<pow-captcha id="register-captcha"></pow-captcha>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
id="generate-uuid"
|
||||
@@ -210,6 +222,16 @@ class AuthModal extends HTMLElement {
|
||||
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.error = null
|
||||
this.render()
|
||||
@@ -219,18 +241,34 @@ class AuthModal extends HTMLElement {
|
||||
this.loading = false
|
||||
|
||||
if (result.success) {
|
||||
this.loginAttempts = 0
|
||||
this.hide()
|
||||
this.dispatchEvent(new CustomEvent('login', { detail: { success: true } }))
|
||||
} else {
|
||||
this.loginAttempts++
|
||||
this.error = result.error || t('auth.invalidUuid')
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
|
||||
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.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()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export class PowCaptcha extends HTMLElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.solved = false
|
||||
this.solving = false
|
||||
this.solution = null
|
||||
this.unsubscribe = null
|
||||
}
|
||||
@@ -21,46 +22,42 @@ export class PowCaptcha extends HTMLElement {
|
||||
|
||||
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 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.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)
|
||||
|
||||
Reference in New Issue
Block a user