fix: security hardening + code quality improvements (401 retry limit, UUID crypto, debounce this-bug, deduplicate CSS/helpers, optimize SW precache)

This commit is contained in:
2026-02-08 13:53:23 +01:00
parent c66c80adcc
commit 9f48e073b8
11 changed files with 41 additions and 152 deletions

View File

@@ -18,7 +18,6 @@ async function initApp() {
// Restore auth session before loading components
await auth.tryRestoreSession()
favoritesService.init()
notificationsService.init()
auth.subscribe((loggedIn) => {
if (loggedIn) {
@@ -28,6 +27,10 @@ async function initApp() {
}
})
if (auth.isLoggedIn()) {
notificationsService.init()
}
await import('./components/app-shell.js')
const appEl = document.getElementById('app')

View File

@@ -73,13 +73,13 @@ export function setupGlobalErrorHandler() {
// Don't show UI for script loading errors
if (event.filename && event.filename.includes('.js')) {
showErrorToast(event.message || 'Ein Fehler ist aufgetreten')
showErrorToast(event.message || t('common.error'))
}
})
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason)
showErrorToast(event.reason?.message || 'Ein Fehler ist aufgetreten')
showErrorToast(event.reason?.message || t('common.error'))
})
// Offline/Online detection

View File

@@ -4,8 +4,6 @@ import { getXmrRates, formatPrice as formatCurrencyPrice } from '../services/cur
import { auth } from '../services/auth.js'
import { favoritesService } from '../services/favorites.js'
let cachedRates = null
class ListingCard extends HTMLElement {
static get observedAttributes() {
return ['listing-id', 'title', 'price', 'currency', 'location', 'image', 'owner-id', 'payment-status', 'status', 'priority']
@@ -53,10 +51,7 @@ class ListingCard extends HTMLElement {
}
async loadRates() {
if (!cachedRates) {
cachedRates = await getXmrRates()
}
this.rates = cachedRates
this.rates = await getXmrRates()
}
attributeChangedCallback() {

View File

@@ -27,11 +27,13 @@ class AuthService {
return crypto.randomUUID()
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
return [...bytes].map((b, i) =>
([4, 6, 8, 10].includes(i) ? '-' : '') + b.toString(16).padStart(2, '0')
).join('')
}
/**

View File

@@ -18,14 +18,13 @@ class ConversationsService {
return this.hashPublicKey(publicKey)
}
hashPublicKey(publicKey) {
async hashPublicKey(publicKey) {
const encoder = new TextEncoder()
const data = encoder.encode(publicKey)
return crypto.subtle.digest('SHA-256', data).then(hash => {
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
})
const hash = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
async getMyConversations() {

View File

@@ -45,6 +45,7 @@ class DirectusService {
this.refreshToken = null
this.tokenExpiry = null
this.refreshTimeout = null
this._refreshPromise = null
this.loadTokens()
this.setupVisibilityRefresh()
@@ -153,15 +154,17 @@ class DirectusService {
headers
})
// Token expired - try refresh (but not for auth endpoints)
if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/')) {
const refreshed = await this.refreshSession()
if (refreshed) {
return this.request(endpoint, options)
} else {
this.clearTokens()
return this.request(endpoint, options)
if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/') && _retryCount < 1) {
if (!this._refreshPromise) {
this._refreshPromise = this.refreshSession().finally(() => {
this._refreshPromise = null
})
}
const refreshed = await this._refreshPromise
if (!refreshed) {
this.clearTokens()
}
return this.request(endpoint, options, _retryCount + 1)
}
if (response.status === 429 && _retryCount < 3) {

View File

@@ -21,34 +21,6 @@ export function escapeHTML(str) {
.replace(/'/g, '&#039;');
}
/**
* Format price with currency symbol
* @param {number} price - Price value
* @param {string} [currency='EUR'] - Currency code (EUR, USD, CHF, XMR)
* @returns {string} Formatted price string
* @example
* formatPrice(99.5, 'EUR') // '€ 99.50'
* formatPrice(0.5, 'XMR') // '0.5000 ɱ'
*/
export function formatPrice(price, currency = 'EUR') {
if (price === null || price === undefined) return '';
const symbols = {
EUR: '€',
USD: '$',
CHF: 'CHF',
XMR: 'ɱ'
};
const symbol = symbols[currency] || currency;
if (currency === 'XMR') {
return `${price.toFixed(4)} ${symbol}`;
}
return `${symbol} ${price.toFixed(2)}`;
}
/**
* Format relative time (e.g., "vor 2 Stunden", "2 hours ago")
* Uses Intl.RelativeTimeFormat for localization
@@ -84,11 +56,11 @@ export function formatRelativeTime(date, locale = 'de') {
* const debouncedSearch = debounce((q) => search(q), 500)
*/
export function debounce(fn, delay = 300) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
let timeoutId
return function (...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), delay)
}
}
/**