implement captcha to register and login
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user