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:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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('')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -21,34 +21,6 @@ export function escapeHTML(str) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user