fix: resolve runtime bugs (i18n export, chat crypto, async getUser, event leak) and remove dead code

This commit is contained in:
2026-02-06 13:44:19 +01:00
parent 5c66ca28b9
commit 1aa723728e
11 changed files with 65 additions and 334 deletions

View File

@@ -38,9 +38,6 @@ class AppShell extends HTMLElement {
`
this.main = this.querySelector('#router-outlet')
// Try to restore session
auth.tryRestoreSession()
}
setupRouter() {

View File

@@ -28,6 +28,8 @@ class PageHome extends HTMLElement {
// Geo state
this.userLat = null
this.userLng = null
this._onHashChange = this.handleHashChange.bind(this)
}
connectedCallback() {
@@ -50,13 +52,13 @@ class PageHome extends HTMLElement {
})
// Listen for URL changes (back/forward navigation)
window.addEventListener('hashchange', this.handleHashChange.bind(this))
window.addEventListener('hashchange', this._onHashChange)
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe()
if (this.authUnsubscribe) this.authUnsubscribe()
window.removeEventListener('hashchange', this.handleHashChange.bind(this))
window.removeEventListener('hashchange', this._onHashChange)
}
handleHashChange() {

View File

@@ -1,7 +1,6 @@
import { t, i18n } from '../../i18n.js'
import { directus } from '../../services/directus.js'
import { auth } from '../../services/auth.js'
import { listingsService } from '../../services/listings.js'
import { getXmrRates, formatPrice as formatCurrencyPrice } from '../../services/currency.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../chat-widget.js'

View File

@@ -5,9 +5,10 @@ class PageSettings extends HTMLElement {
constructor() {
super()
this.isLoggedIn = false
this.user = null
}
connectedCallback() {
async connectedCallback() {
this.isLoggedIn = auth.isLoggedIn()
if (!this.isLoggedIn) {
@@ -15,6 +16,7 @@ class PageSettings extends HTMLElement {
return
}
this.user = await auth.getUser()
this.render()
this.setupEventListeners()
@@ -137,7 +139,7 @@ class PageSettings extends HTMLElement {
render() {
const currentTheme = this.getCurrentTheme()
const currentLang = i18n.getLocale()
const user = auth.getUser()
const user = this.user
this.innerHTML = /* html */`
<div class="settings-page">

View File

@@ -206,3 +206,4 @@ class I18n {
export const i18n = new I18n()
export const t = (key, params) => i18n.t(key, params)
export const getCurrentLanguage = () => i18n.getLocale()

View File

@@ -76,11 +76,7 @@ class ConversationsService {
let text = '[Encrypted]'
try {
if (isOwn) {
text = cryptoService.decryptOwn(msg.content_encrypted, msg.nonce)
} else {
text = cryptoService.decrypt(msg.content_encrypted, msg.nonce, otherPublicKey)
}
text = cryptoService.decrypt(msg.content_encrypted, msg.nonce, otherPublicKey)
} catch (e) {
text = '[Decryption failed]'
}

View File

@@ -85,13 +85,9 @@ class CryptoService {
const nonce = this.nacl.randomBytes(this.nacl.box.nonceLength)
const messageUint8 = this.naclUtil.decodeUTF8(message)
const recipientKey = this.naclUtil.decodeBase64(recipientPublicKey)
const sharedKey = this.nacl.box.before(recipientKey, this.keyPair.secretKey)
const encrypted = this.nacl.box(
messageUint8,
nonce,
recipientKey,
this.keyPair.secretKey
)
const encrypted = this.nacl.secretbox(messageUint8, nonce, sharedKey)
return {
nonce: this.naclUtil.encodeBase64(nonce),
@@ -103,16 +99,18 @@ class CryptoService {
* Decrypt a message from a sender
* @param {string} ciphertext - Base64 encoded ciphertext
* @param {string} nonce - Base64 encoded nonce
* @param {string} senderPublicKey - Base64 encoded public key
* @param {string} otherPublicKey - Base64 encoded public key of the other party
* @returns {string|null} - Decrypted message or null if failed
*/
decrypt(ciphertext, nonce, senderPublicKey) {
decrypt(ciphertext, nonce, otherPublicKey) {
try {
const decrypted = this.nacl.box.open(
const otherKey = this.naclUtil.decodeBase64(otherPublicKey)
const sharedKey = this.nacl.box.before(otherKey, this.keyPair.secretKey)
const decrypted = this.nacl.secretbox.open(
this.naclUtil.decodeBase64(ciphertext),
this.naclUtil.decodeBase64(nonce),
this.naclUtil.decodeBase64(senderPublicKey),
this.keyPair.secretKey
sharedKey
)
if (!decrypted) return null

View File

@@ -480,15 +480,6 @@ class DirectusService {
})
}
async incrementViews(id) {
const listing = await this.getListing(id)
if (listing) {
return this.patch(`/items/listings/${id}`, {
views: (listing.views || 0) + 1
})
}
}
// ==================== Categories (Kategorien) ====================
async getCategories() {
@@ -679,58 +670,6 @@ class DirectusService {
return response.data
}
// ==================== Favorites (Favoriten) ====================
async getFavorites() {
const response = await this.get('/items/favorites', {
fields: ['*', 'listing.*', 'listing.images.directus_files_id.*'],
filter: { user: { _eq: '$CURRENT_USER' } }
})
return response.data
}
async addFavorite(listingId) {
const response = await this.post('/items/favorites', {
listing: listingId
})
return response.data
}
async removeFavorite(favoriteId) {
return this.delete(`/items/favorites/${favoriteId}`)
}
async isFavorite(listingId) {
const response = await this.get('/items/favorites', {
filter: {
user: { _eq: '$CURRENT_USER' },
listing: { _eq: listingId }
},
limit: 1
})
return response.data.length > 0 ? response.data[0] : null
}
// ==================== Reports (Meldungen) ====================
async reportListing(listingId, reason, details = '') {
const response = await this.post('/items/reports', {
listing: listingId,
reason,
details
})
return response.data
}
async reportUser(userId, reason, details = '') {
const response = await this.post('/items/reports', {
reported_user: userId,
reason,
details
})
return response.data
}
// ==================== Files (Dateien/Bilder) ====================
/**
@@ -796,52 +735,6 @@ class DirectusService {
return this.getFileUrl(fileId, { width: size, height: size, fit: 'cover' })
}
// ==================== Search ====================
async globalSearch(query, options = {}) {
const [listings, categories] = await Promise.all([
this.searchListings(query, { limit: options.listingLimit || 10 }),
this.get('/items/categories', {
search: query,
limit: options.categoryLimit || 5
})
])
return {
listings: listings.items,
categories: categories.data
}
}
// ==================== Stats / Dashboard ====================
async getUserStats() {
const [listings, favorites, conversations] = await Promise.all([
this.get('/items/listings', {
filter: { user_created: { _eq: '$CURRENT_USER' } },
aggregate: { count: '*' }
}),
this.get('/items/favorites', {
filter: { user: { _eq: '$CURRENT_USER' } },
aggregate: { count: '*' }
}),
this.get('/items/conversations', {
filter: {
_or: [
{ buyer: { _eq: '$CURRENT_USER' } },
{ seller: { _eq: '$CURRENT_USER' } }
]
},
aggregate: { count: '*' }
})
])
return {
listingsCount: listings.data?.[0]?.count || 0,
favoritesCount: favorites.data?.[0]?.count || 0,
conversationsCount: conversations.data?.[0]?.count || 0
}
}
}
class DirectusError extends Error {

View File

@@ -4,46 +4,8 @@
*/
import { directus } from './directus.js'
import currency from './currency.js'
class ListingsService {
constructor() {
this.cache = new Map()
this.cacheTimeout = 5 * 60 * 1000 // 5 minutes
}
async getFeaturedListings(limit = 8) {
return directus.getListings({
filter: {
status: { _eq: 'published' }
},
sort: ['-views', '-date_created'],
limit
})
}
async getRecentListings(limit = 12) {
return directus.getListings({
filter: { status: { _eq: 'published' } },
sort: ['-date_created'],
limit
})
}
async getListingWithPriceConversion(id, targetCurrency = 'EUR') {
const listing = await directus.getListing(id)
if (!listing) return null
if (listing.price && (listing.price_mode === 'xmr' || listing.currency === 'XMR')) {
const rates = await currency.getXmrRates()
const fiatPrice = currency.convertFromXmr(listing.price, targetCurrency, rates)
listing.price_converted = fiatPrice
listing.price_converted_currency = targetCurrency
}
return listing
}
async getListingsWithFilters(filters = {}) {
const directusFilter = { status: { _eq: 'published' } }
@@ -93,139 +55,6 @@ class ListingsService {
search: filters.search
})
}
async getSimilarListings(listing, limit = 4) {
if (!listing) return []
const response = await directus.getListings({
filter: {
status: { _eq: 'published' },
id: { _neq: listing.id },
category: { _eq: listing.category?.id || listing.category }
},
limit
})
return response.items || []
}
async getUserListings(userId) {
const response = await directus.get('/items/listings', {
fields: ['*', 'images.directus_files_id.id', 'category.name', 'location.name'],
filter: { user_created: { _eq: userId } },
sort: ['-date_created']
})
return response.data
}
async createListingWithImages(data, imageFiles) {
let images = []
if (imageFiles && imageFiles.length > 0) {
const uploadedFiles = await directus.uploadMultipleFiles(imageFiles)
images = uploadedFiles.map((file, index) => ({
directus_files_id: file.id,
sort: index
}))
}
const listingData = {
...data,
status: 'draft',
images: images.length > 0 ? { create: images } : undefined
}
return directus.createListing(listingData)
}
async publishListing(id) {
return directus.updateListing(id, { status: 'published' })
}
async unpublishListing(id) {
return directus.updateListing(id, { status: 'draft' })
}
async archiveListing(id) {
return directus.updateListing(id, { status: 'archived' })
}
formatPrice(listing, locale = 'de-DE') {
if (!listing || listing.price === null || listing.price === undefined) {
return null
}
if (listing.price_type === 'negotiable') {
return `${this.formatAmount(listing.price, listing.currency, locale)} VB`
}
if (listing.price_type === 'free') {
return 'Gratis'
}
return this.formatAmount(listing.price, listing.currency, locale)
}
formatAmount(amount, currency = 'EUR', locale = 'de-DE') {
if (currency === 'XMR') {
return `${parseFloat(amount).toFixed(4)} XMR`
}
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(amount)
}
getConditionLabel(condition, lang = 'de') {
const labels = {
de: {
new: 'Neu',
like_new: 'Wie neu',
good: 'Gut',
fair: 'Akzeptabel',
poor: 'Stark gebraucht'
},
en: {
new: 'New',
like_new: 'Like new',
good: 'Good',
fair: 'Fair',
poor: 'Poor'
},
fr: {
new: 'Neuf',
like_new: 'Comme neuf',
good: 'Bon',
fair: 'Acceptable',
poor: 'Usagé'
}
}
return labels[lang]?.[condition] || condition
}
getPriceTypeLabel(priceType, lang = 'de') {
const labels = {
de: {
fixed: 'Festpreis',
negotiable: 'Verhandelbar',
free: 'Zu verschenken'
},
en: {
fixed: 'Fixed price',
negotiable: 'Negotiable',
free: 'Free'
},
fr: {
fixed: 'Prix fixe',
negotiable: 'Négociable',
free: 'Gratuit'
}
}
return labels[lang]?.[priceType] || priceType
}
}
export const listingsService = new ListingsService()

View File

@@ -2,9 +2,9 @@
// 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)
// TODO: Replace with a server-side endpoint. Currently generates challenge
// client-side with a btoa() "signature" that provides no real security.
export function generateChallenge() {
const challenge = crypto.randomUUID()
const timestamp = Date.now()
@@ -12,35 +12,10 @@ export function generateChallenge() {
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