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:
@@ -12,8 +12,8 @@
|
||||
--color-secondary: #777777;
|
||||
--color-secondary-hover: #5A5A5A;
|
||||
|
||||
--color-accent: #047857;
|
||||
--color-accent-hover: #065f46;
|
||||
--color-accent: #059669;
|
||||
--color-accent-hover: #047857;
|
||||
--color-accent-text: #fff;
|
||||
|
||||
--color-success: #666666;
|
||||
@@ -160,33 +160,3 @@
|
||||
--color-overlay: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Light Mode - Manual Override */
|
||||
[data-theme="light"] {
|
||||
--color-primary: #555555;
|
||||
--color-primary-hover: #404040;
|
||||
--color-primary-light: #E8E8E8;
|
||||
|
||||
--color-secondary: #777777;
|
||||
--color-secondary-hover: #5A5A5A;
|
||||
|
||||
--color-accent: #059669;
|
||||
--color-accent-hover: #047857;
|
||||
|
||||
--color-success: #666666;
|
||||
--color-warning: #888888;
|
||||
--color-error: #444444;
|
||||
|
||||
--color-bg: #FAFAFA;
|
||||
--color-bg-secondary: #F0F0F0;
|
||||
--color-bg-tertiary: #E5E5E5;
|
||||
|
||||
--color-text: #1A1A1A;
|
||||
--color-text-secondary: #4A4A4A;
|
||||
--color-text-muted: #737373;
|
||||
|
||||
--color-border: #D0D0D0;
|
||||
--color-border-focus: #555555;
|
||||
|
||||
--color-shadow: rgba(0, 0, 0, 0.1);
|
||||
--color-overlay: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
12
index.html
12
index.html
@@ -62,7 +62,7 @@
|
||||
:root {
|
||||
--color-primary: #555555; --color-primary-hover: #404040; --color-primary-light: #E8E8E8;
|
||||
--color-secondary: #777777; --color-secondary-hover: #5A5A5A;
|
||||
--color-accent: #047857; --color-accent-hover: #065f46; --color-accent-text: #fff;
|
||||
--color-accent: #059669; --color-accent-hover: #047857; --color-accent-text: #fff;
|
||||
--color-success: #666666; --color-warning: #888888; --color-error: #444444;
|
||||
--color-bg: #FAFAFA; --color-bg-secondary: #F0F0F0; --color-bg-tertiary: #E5E5E5;
|
||||
--color-text: #1A1A1A; --color-text-secondary: #4A4A4A; --color-text-muted: #737373;
|
||||
@@ -102,16 +102,6 @@
|
||||
--color-border: #3A3A3A; --color-border-focus: #AAAAAA;
|
||||
--color-shadow: rgba(0, 0, 0, 0.4); --color-overlay: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
[data-theme="light"] {
|
||||
--color-primary: #555555; --color-primary-hover: #404040; --color-primary-light: #E8E8E8;
|
||||
--color-secondary: #777777; --color-secondary-hover: #5A5A5A;
|
||||
--color-accent: #059669; --color-accent-hover: #047857;
|
||||
--color-success: #666666; --color-warning: #888888; --color-error: #444444;
|
||||
--color-bg: #FAFAFA; --color-bg-secondary: #F0F0F0; --color-bg-tertiary: #E5E5E5;
|
||||
--color-text: #1A1A1A; --color-text-secondary: #4A4A4A; --color-text-muted: #737373;
|
||||
--color-border: #D0D0D0; --color-border-focus: #555555;
|
||||
--color-shadow: rgba(0, 0, 0, 0.1); --color-overlay: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* base.css */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
@@ -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 => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = 'dgray-v49';
|
||||
const CACHE_NAME = 'dgray-v50';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
@@ -45,20 +45,9 @@ const STATIC_ASSETS = [
|
||||
'/js/components/location-map.js',
|
||||
'/js/components/pow-captcha.js',
|
||||
|
||||
// Pages
|
||||
// Pages (critical only — lazy-loaded pages are runtime-cached via stale-while-revalidate)
|
||||
'/js/components/pages/page-home.js',
|
||||
'/js/components/pages/page-listing.js',
|
||||
'/js/components/pages/page-create.js',
|
||||
'/js/components/pages/page-favorites.js',
|
||||
'/js/components/pages/page-my-listings.js',
|
||||
'/js/components/pages/page-messages.js',
|
||||
'/js/components/pages/page-settings.js',
|
||||
'/js/components/pages/page-not-found.js',
|
||||
'/js/components/pages/page-notifications.js',
|
||||
'/js/components/pages/page-privacy.js',
|
||||
'/js/components/pages/page-terms.js',
|
||||
'/js/components/pages/page-about.js',
|
||||
'/js/components/pages/page-contact.js',
|
||||
|
||||
// Vendor
|
||||
'/js/vendor/cropper.min.js',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { test, describe, assertEquals, assertTrue } from './test-runner.js'
|
||||
import { escapeHTML, formatPrice, formatRelativeTime, debounce, truncate } from '../js/utils/helpers.js'
|
||||
import { escapeHTML, formatRelativeTime, debounce, truncate } from '../js/utils/helpers.js'
|
||||
|
||||
describe('escapeHTML', () => {
|
||||
test('escapes < and >', () => {
|
||||
@@ -40,40 +40,6 @@ describe('escapeHTML', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPrice', () => {
|
||||
test('formats EUR correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'EUR'), '€ 99.50')
|
||||
})
|
||||
|
||||
test('formats USD correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'USD'), '$ 99.50')
|
||||
})
|
||||
|
||||
test('formats CHF correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'CHF'), 'CHF 99.50')
|
||||
})
|
||||
|
||||
test('formats XMR with 4 decimals', () => {
|
||||
assertEquals(formatPrice(0.5, 'XMR'), '0.5000 ɱ')
|
||||
})
|
||||
|
||||
test('handles null price', () => {
|
||||
assertEquals(formatPrice(null, 'EUR'), '–')
|
||||
})
|
||||
|
||||
test('handles undefined price', () => {
|
||||
assertEquals(formatPrice(undefined, 'EUR'), '–')
|
||||
})
|
||||
|
||||
test('defaults to EUR', () => {
|
||||
assertEquals(formatPrice(10), '€ 10.00')
|
||||
})
|
||||
|
||||
test('handles unknown currency', () => {
|
||||
assertEquals(formatPrice(10, 'GBP'), 'GBP 10.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatRelativeTime', () => {
|
||||
test('formats seconds ago', () => {
|
||||
const date = new Date(Date.now() - 30000) // 30 seconds ago
|
||||
|
||||
Reference in New Issue
Block a user