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: #777777;
|
||||||
--color-secondary-hover: #5A5A5A;
|
--color-secondary-hover: #5A5A5A;
|
||||||
|
|
||||||
--color-accent: #047857;
|
--color-accent: #059669;
|
||||||
--color-accent-hover: #065f46;
|
--color-accent-hover: #047857;
|
||||||
--color-accent-text: #fff;
|
--color-accent-text: #fff;
|
||||||
|
|
||||||
--color-success: #666666;
|
--color-success: #666666;
|
||||||
@@ -160,33 +160,3 @@
|
|||||||
--color-overlay: rgba(0, 0, 0, 0.7);
|
--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 {
|
:root {
|
||||||
--color-primary: #555555; --color-primary-hover: #404040; --color-primary-light: #E8E8E8;
|
--color-primary: #555555; --color-primary-hover: #404040; --color-primary-light: #E8E8E8;
|
||||||
--color-secondary: #777777; --color-secondary-hover: #5A5A5A;
|
--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-success: #666666; --color-warning: #888888; --color-error: #444444;
|
||||||
--color-bg: #FAFAFA; --color-bg-secondary: #F0F0F0; --color-bg-tertiary: #E5E5E5;
|
--color-bg: #FAFAFA; --color-bg-secondary: #F0F0F0; --color-bg-tertiary: #E5E5E5;
|
||||||
--color-text: #1A1A1A; --color-text-secondary: #4A4A4A; --color-text-muted: #737373;
|
--color-text: #1A1A1A; --color-text-secondary: #4A4A4A; --color-text-muted: #737373;
|
||||||
@@ -102,16 +102,6 @@
|
|||||||
--color-border: #3A3A3A; --color-border-focus: #AAAAAA;
|
--color-border: #3A3A3A; --color-border-focus: #AAAAAA;
|
||||||
--color-shadow: rgba(0, 0, 0, 0.4); --color-overlay: rgba(0, 0, 0, 0.7);
|
--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 */
|
/* base.css */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ async function initApp() {
|
|||||||
// Restore auth session before loading components
|
// Restore auth session before loading components
|
||||||
await auth.tryRestoreSession()
|
await auth.tryRestoreSession()
|
||||||
favoritesService.init()
|
favoritesService.init()
|
||||||
notificationsService.init()
|
|
||||||
|
|
||||||
auth.subscribe((loggedIn) => {
|
auth.subscribe((loggedIn) => {
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -28,6 +27,10 @@ async function initApp() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (auth.isLoggedIn()) {
|
||||||
|
notificationsService.init()
|
||||||
|
}
|
||||||
|
|
||||||
await import('./components/app-shell.js')
|
await import('./components/app-shell.js')
|
||||||
|
|
||||||
const appEl = document.getElementById('app')
|
const appEl = document.getElementById('app')
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ export function setupGlobalErrorHandler() {
|
|||||||
|
|
||||||
// Don't show UI for script loading errors
|
// Don't show UI for script loading errors
|
||||||
if (event.filename && event.filename.includes('.js')) {
|
if (event.filename && event.filename.includes('.js')) {
|
||||||
showErrorToast(event.message || 'Ein Fehler ist aufgetreten')
|
showErrorToast(event.message || t('common.error'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
console.error('Unhandled promise rejection:', event.reason)
|
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
|
// Offline/Online detection
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { getXmrRates, formatPrice as formatCurrencyPrice } from '../services/cur
|
|||||||
import { auth } from '../services/auth.js'
|
import { auth } from '../services/auth.js'
|
||||||
import { favoritesService } from '../services/favorites.js'
|
import { favoritesService } from '../services/favorites.js'
|
||||||
|
|
||||||
let cachedRates = null
|
|
||||||
|
|
||||||
class ListingCard extends HTMLElement {
|
class ListingCard extends HTMLElement {
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ['listing-id', 'title', 'price', 'currency', 'location', 'image', 'owner-id', 'payment-status', 'status', 'priority']
|
return ['listing-id', 'title', 'price', 'currency', 'location', 'image', 'owner-id', 'payment-status', 'status', 'priority']
|
||||||
@@ -53,10 +51,7 @@ class ListingCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadRates() {
|
async loadRates() {
|
||||||
if (!cachedRates) {
|
this.rates = await getXmrRates()
|
||||||
cachedRates = await getXmrRates()
|
|
||||||
}
|
|
||||||
this.rates = cachedRates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback() {
|
attributeChangedCallback() {
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ class AuthService {
|
|||||||
return crypto.randomUUID()
|
return crypto.randomUUID()
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
const bytes = new Uint8Array(16)
|
||||||
const r = Math.random() * 16 | 0
|
crypto.getRandomValues(bytes)
|
||||||
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||||
return v.toString(16)
|
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)
|
return this.hashPublicKey(publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashPublicKey(publicKey) {
|
async hashPublicKey(publicKey) {
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
const data = encoder.encode(publicKey)
|
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))
|
return Array.from(new Uint8Array(hash))
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join('')
|
.join('')
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMyConversations() {
|
async getMyConversations() {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class DirectusService {
|
|||||||
this.refreshToken = null
|
this.refreshToken = null
|
||||||
this.tokenExpiry = null
|
this.tokenExpiry = null
|
||||||
this.refreshTimeout = null
|
this.refreshTimeout = null
|
||||||
|
this._refreshPromise = null
|
||||||
|
|
||||||
this.loadTokens()
|
this.loadTokens()
|
||||||
this.setupVisibilityRefresh()
|
this.setupVisibilityRefresh()
|
||||||
@@ -153,15 +154,17 @@ class DirectusService {
|
|||||||
headers
|
headers
|
||||||
})
|
})
|
||||||
|
|
||||||
// Token expired - try refresh (but not for auth endpoints)
|
if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/') && _retryCount < 1) {
|
||||||
if (response.status === 401 && this.refreshToken && !endpoint.startsWith('/auth/')) {
|
if (!this._refreshPromise) {
|
||||||
const refreshed = await this.refreshSession()
|
this._refreshPromise = this.refreshSession().finally(() => {
|
||||||
if (refreshed) {
|
this._refreshPromise = null
|
||||||
return this.request(endpoint, options)
|
})
|
||||||
} else {
|
|
||||||
this.clearTokens()
|
|
||||||
return this.request(endpoint, options)
|
|
||||||
}
|
}
|
||||||
|
const refreshed = await this._refreshPromise
|
||||||
|
if (!refreshed) {
|
||||||
|
this.clearTokens()
|
||||||
|
}
|
||||||
|
return this.request(endpoint, options, _retryCount + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 429 && _retryCount < 3) {
|
if (response.status === 429 && _retryCount < 3) {
|
||||||
|
|||||||
@@ -21,34 +21,6 @@ export function escapeHTML(str) {
|
|||||||
.replace(/'/g, ''');
|
.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")
|
* Format relative time (e.g., "vor 2 Stunden", "2 hours ago")
|
||||||
* Uses Intl.RelativeTimeFormat for localization
|
* Uses Intl.RelativeTimeFormat for localization
|
||||||
@@ -84,11 +56,11 @@ export function formatRelativeTime(date, locale = 'de') {
|
|||||||
* const debouncedSearch = debounce((q) => search(q), 500)
|
* const debouncedSearch = debounce((q) => search(q), 500)
|
||||||
*/
|
*/
|
||||||
export function debounce(fn, delay = 300) {
|
export function debounce(fn, delay = 300) {
|
||||||
let timeoutId;
|
let timeoutId
|
||||||
return (...args) => {
|
return function (...args) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId)
|
||||||
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
timeoutId = setTimeout(() => fn.apply(this, args), delay)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'dgray-v49';
|
const CACHE_NAME = 'dgray-v50';
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
@@ -45,20 +45,9 @@ const STATIC_ASSETS = [
|
|||||||
'/js/components/location-map.js',
|
'/js/components/location-map.js',
|
||||||
'/js/components/pow-captcha.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-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-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
|
// Vendor
|
||||||
'/js/vendor/cropper.min.js',
|
'/js/vendor/cropper.min.js',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, describe, assertEquals, assertTrue } from './test-runner.js'
|
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', () => {
|
describe('escapeHTML', () => {
|
||||||
test('escapes < and >', () => {
|
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', () => {
|
describe('formatRelativeTime', () => {
|
||||||
test('formats seconds ago', () => {
|
test('formats seconds ago', () => {
|
||||||
const date = new Date(Date.now() - 30000) // 30 seconds ago
|
const date = new Date(Date.now() - 30000) // 30 seconds ago
|
||||||
|
|||||||
Reference in New Issue
Block a user