From 3669321139aa5a9c0d96de7c3f14e1a18b0c1319 Mon Sep 17 00:00:00 2001 From: Alexander Schmidt Date: Tue, 3 Feb 2026 14:44:36 +0100 Subject: [PATCH] add pow-captcha --- docs/MONETIZATION.md | 45 ++++++--- js/components/pages/page-create.js | 12 +++ js/components/pow-captcha.js | 156 +++++++++++++++++++++++++++++ js/services/pow-captcha.js | 81 +++++++++++++++ locales/de.json | 7 ++ locales/en.json | 7 ++ locales/fr.json | 7 ++ 7 files changed, 300 insertions(+), 15 deletions(-) create mode 100644 js/components/pow-captcha.js create mode 100644 js/services/pow-captcha.js diff --git a/docs/MONETIZATION.md b/docs/MONETIZATION.md index bf71a91..75e893c 100644 --- a/docs/MONETIZATION.md +++ b/docs/MONETIZATION.md @@ -2,7 +2,7 @@ ## Preismodell -**1 Anzeige = 1 Monat = 1 EUR/CHF/USD** +**1 Anzeige = 1 Monat = 1 EUR/USD/CHF/GBP (oder 200 JPY)** - Einfach, transparent, fair - Power-User (mit Reputation): 2 Monate Laufzeit @@ -14,13 +14,17 @@ - Anzeigen von Accounts mit erfolgreichen Verkäufen werden höher gerankt - Neue Accounts erscheinen weiter unten in Suchergebnissen -### Trust-Badge -- "Verifizierter Verkäufer" nach X abgeschlossenen Deals -- Käufer bestätigt erfolgreichen Abschluss +### Reputation-Stufen -### Power-User Vorteile -- 2 Monate Laufzeit statt 1 Monat (bei gleichem Preis) -- Kriterien: z.B. 5+ erfolgreiche Verkäufe +| Stufe | Deals | Vorteile | +|-------|-------|----------| +| Neu | 0 | Standard (1 Monat Laufzeit) | +| Verifizierter Verkäufer | 5+ | Badge, bessere Sichtbarkeit | +| Power-User | 15+ | 2 Monate Laufzeit, höheres Ranking | +| Top Seller | 50+ | Badge, Priority-Ranking, maximale Sichtbarkeit | + +- Käufer bestätigt erfolgreichen Abschluss +- Deals werden erst nach Käufer-Bestätigung gezählt ### Proof of Wallet (optional) - Einmalige Monero-Mikrozahlung als Verifikation @@ -33,20 +37,31 @@ - Max. 3 neue Anzeigen pro Tag für neue Accounts - Limit erhöht sich mit Reputation -### Captcha +### Captcha (Eigenes PoW) +- 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 verdächtigen Aktivitäten +- Bei Anzeigen-Erstellung +- Implementierung: `js/services/pow-captcha.js`, `js/components/pow-captcha.js` ## Payment-Integration - **Provider**: BTCpay Server (self-hosted) -- **Währung**: Monero (XMR) -- **Preisumrechnung**: Live XMR/EUR-Kurs zum Zeitpunkt der Zahlung +- **URL**: https://pay.xmr.rocks/ +- **Primär**: Monero (XMR) +- **Alternativ**: Andere Kryptos via Trocador-Plugin (automatischer Swap zu XMR) +- **Preisumrechnung**: Live XMR-Kurs via Kraken API + - EUR: `https://api.kraken.com/0/public/Ticker?pair=XMREUR` + - USD: `https://api.kraken.com/0/public/Ticker?pair=XMRUSD` + - CHF: `https://api.kraken.com/0/public/Ticker?pair=XMRCHF` + - GBP: `https://api.kraken.com/0/public/Ticker?pair=XMRGBP` + - JPY: `https://api.kraken.com/0/public/Ticker?pair=XMRJPY` - **Bestätigung**: Nach 1-2 Blockchain-Confirmations ## Offene Fragen -- [ ] BTCpay Server Setup & Hosting -- [ ] XMR-Kurs API für Umrechnung -- [ ] Anzahl Deals für Power-User Status -- [ ] Captcha-Lösung (privacy-freundlich, z.B. hCaptcha) +- [x] ~~BTCpay Server Setup & Hosting~~ → https://pay.xmr.rocks/ +- [x] ~~XMR-Kurs API für Umrechnung~~ → Kraken API +- [x] ~~Anzahl Deals für Power-User Status~~ → 5/15/50 Stufen +- [x] ~~Captcha-Lösung~~ → Eigenes PoW-Captcha (keine Lizenzkosten) diff --git a/js/components/pages/page-create.js b/js/components/pages/page-create.js index 08f6da1..dc4fd40 100644 --- a/js/components/pages/page-create.js +++ b/js/components/pages/page-create.js @@ -4,6 +4,7 @@ import { auth } from '../../services/auth.js' import { directus } from '../../services/directus.js' import { SUPPORTED_CURRENCIES } from '../../services/currency.js' import '../location-picker.js' +import '../pow-captcha.js' const STORAGE_KEY = 'dgray_create_draft' @@ -247,6 +248,10 @@ class PageCreate extends HTMLElement { >

${t('create.moneroHint')}

+ +
+ +
` + } +
+ + + ` + + if (!this.solved) { + this.querySelector('.pow-captcha-btn').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') + + btn.disabled = true + btn.textContent = t('captcha.solving') + progress.style.display = 'flex' + + 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')}` + }) + + // Store solution for form submission + this.solution = { + challenge, + nonce: result.nonce, + signature, + timestamp + } + + this.solved = true + this.dispatchEvent(new CustomEvent('solved', { detail: this.solution })) + this.render() + + } catch (error) { + btn.disabled = false + btn.textContent = t('captcha.error') + 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 { + padding: var(--space-md); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-surface); + } + + .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); + cursor: pointer; + font-size: var(--font-size-sm); + } + + .pow-captcha-btn:disabled { + opacity: 0.6; + cursor: wait; + } + + .pow-captcha-success { + 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) diff --git a/js/services/pow-captcha.js b/js/services/pow-captcha.js new file mode 100644 index 0000000..81b0703 --- /dev/null +++ b/js/services/pow-captcha.js @@ -0,0 +1,81 @@ +// Proof-of-Work Captcha Service +// Client must find nonce where SHA256(challenge + nonce) has N leading zeros + +const DIFFICULTY = 4 // Number of leading zeros required (4 = ~65k attempts avg) +const CHALLENGE_EXPIRY = 5 * 60 * 1000 // 5 minutes + +// Generate a challenge (call this from your API/backend) +export function generateChallenge() { + const challenge = crypto.randomUUID() + const timestamp = Date.now() + return { + challenge, + difficulty: DIFFICULTY, + timestamp, + // Sign to prevent tampering (simple HMAC alternative) + signature: btoa(`${challenge}:${timestamp}:${DIFFICULTY}`) + } +} + +// Verify solution (call this from your API/backend) +export async function verifySolution(challenge, nonce, signature, timestamp) { + // Check expiry + if (Date.now() - timestamp > CHALLENGE_EXPIRY) { + return { valid: false, error: 'Challenge expired' } + } + + // Verify signature + const expectedSig = btoa(`${challenge}:${timestamp}:${DIFFICULTY}`) + if (signature !== expectedSig) { + return { valid: false, error: 'Invalid signature' } + } + + // Verify PoW + const hash = await sha256(`${challenge}${nonce}`) + const prefix = '0'.repeat(DIFFICULTY) + + if (hash.startsWith(prefix)) { + return { valid: true } + } + + return { valid: false, error: 'Invalid proof of work' } +} + +// Solve challenge (runs in browser) +export async function solveChallenge(challenge, difficulty, onProgress) { + let nonce = 0 + const prefix = '0'.repeat(difficulty) + const startTime = Date.now() + + while (true) { + const hash = await sha256(`${challenge}${nonce}`) + + if (hash.startsWith(prefix)) { + return { + nonce, + hash, + duration: Date.now() - startTime + } + } + + nonce++ + + // Report progress every 1000 attempts + if (onProgress && nonce % 1000 === 0) { + onProgress({ attempts: nonce, elapsed: Date.now() - startTime }) + } + + // Yield to main thread every 100 attempts + if (nonce % 100 === 0) { + await new Promise(r => setTimeout(r, 0)) + } + } +} + +// SHA256 helper +async function sha256(message) { + const msgBuffer = new TextEncoder().encode(message) + const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer) + const hashArray = Array.from(new Uint8Array(hashBuffer)) + return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') +} diff --git a/locales/de.json b/locales/de.json index 7db9508..8d60eb1 100644 --- a/locales/de.json +++ b/locales/de.json @@ -173,6 +173,13 @@ "message": "Die gesuchte Seite existiert leider nicht.", "backHome": "Zur Startseite" }, + "captcha": { + "verify": "Ich bin kein Roboter", + "verified": "Verifiziert", + "solving": "Verifiziere...", + "attempts": "Versuche", + "error": "Fehler - erneut versuchen" + }, "auth": { "login": "Anmelden", "logout": "Abmelden", diff --git a/locales/en.json b/locales/en.json index 7b44a3c..da38273 100644 --- a/locales/en.json +++ b/locales/en.json @@ -173,6 +173,13 @@ "message": "The page you are looking for does not exist.", "backHome": "Back to Home" }, + "captcha": { + "verify": "I'm not a robot", + "verified": "Verified", + "solving": "Verifying...", + "attempts": "attempts", + "error": "Error - try again" + }, "auth": { "login": "Login", "logout": "Logout", diff --git a/locales/fr.json b/locales/fr.json index aa43550..c45f9c1 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -173,6 +173,13 @@ "message": "La page que vous recherchez n'existe pas.", "backHome": "Retour à l'accueil" }, + "captcha": { + "verify": "Je ne suis pas un robot", + "verified": "Vérifié", + "solving": "Vérification...", + "attempts": "tentatives", + "error": "Erreur - réessayer" + }, "auth": { "login": "Connexion", "logout": "Déconnexion",