From 743620280fa43214689bc0c81628824b07137ac9 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Mon, 4 Aug 2025 22:49:40 +0200 Subject: [PATCH 1/6] fix: handle localStorage SecurityError gracefully MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add try-catch blocks around localStorage access in authLocalStorage - Return null instead of throwing when localStorage is unavailable - Prevent endless loading screen in private browsing/strict privacy modes - Add proper error logging with console.warn - Fix nullish coalescing operator usage Fixes #212 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app/src/api/directus.ts | 31 ++++++++++++++++++++--------- app/src/api/userApi.ts | 7 ++++--- lib/src/Components/Auth/useAuth.tsx | 2 ++ 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/src/api/directus.ts b/app/src/api/directus.ts index 7b32f8b42..13bb5f816 100644 --- a/app/src/api/directus.ts +++ b/app/src/api/directus.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable no-catch-all/no-catch-all */ +/* eslint-disable no-console */ import { createDirectus, rest, authentication } from '@directus/sdk' import type { AuthenticationData, AuthenticationStorage } from '@directus/sdk' @@ -74,24 +75,36 @@ export const authLocalStorage = (mainKey = 'directus_storage') => ({ // implementation of get, here return json parsed data from localStorage at mainKey (or null if not found) get: async () => { - const data = window.localStorage.getItem(mainKey) - if (data) { - return JSON.parse(data) + try { + const data = window.localStorage.getItem(mainKey) + if (data) { + return JSON.parse(data) + } + return null + } catch (error) { + // Handle SecurityError when localStorage is not available (e.g., in private browsing mode) + console.warn('localStorage not available:', error) + return null } - return null }, // implementation of set, here set the value at mainKey in localStorage, or remove it if value is null set: async (value: AuthenticationData | null) => { - if (!value) { - return window.localStorage.removeItem(mainKey) + try { + if (!value) { + return window.localStorage.removeItem(mainKey) + } + return window.localStorage.setItem(mainKey, JSON.stringify(value)) + } catch (error) { + // Handle SecurityError when localStorage is not available (e.g., in private browsing mode) + console.warn('localStorage not available:', error) + // Silently fail - authentication will fall back to memory-only storage } - return window.localStorage.setItem(mainKey, JSON.stringify(value)) }, }) as AuthenticationStorage export async function getRefreshToken() { const auth = await authLocalStorage().get() - return auth!.refresh_token + return auth?.refresh_token ?? null } export const directusClient = createDirectus('https://api.utopia-lab.org/') diff --git a/app/src/api/userApi.ts b/app/src/api/userApi.ts index c63e1baa1..fc114b6c5 100644 --- a/app/src/api/userApi.ts +++ b/app/src/api/userApi.ts @@ -4,6 +4,7 @@ /* eslint-disable no-console */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable no-catch-all/no-catch-all */ import { createUser, passwordRequest, passwordReset, readMe, updateMe } from '@directus/sdk' import { directusClient } from './directus' @@ -78,9 +79,9 @@ export class UserApi { const token = await directusClient.getToken() return token } catch (error: any) { - console.log(error) - if (error.errors[0].message) throw error.errors[0].message - else throw error + console.warn('Failed to get token:', error) + // Don't throw error - return null instead to allow graceful fallback + return null } } diff --git a/lib/src/Components/Auth/useAuth.tsx b/lib/src/Components/Auth/useAuth.tsx index 413e88f58..be8ccc286 100644 --- a/lib/src/Components/Auth/useAuth.tsx +++ b/lib/src/Components/Auth/useAuth.tsx @@ -64,10 +64,12 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { setLoading(false) return me } else { + setLoading(false) return undefined } // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { + console.warn('Failed to load user token:', error) setLoading(false) return undefined } finally { From c4131ced0f211ca9c9813339dbad46f36ec5f623 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Tue, 5 Aug 2025 09:19:03 +0200 Subject: [PATCH 2/6] fix: add eslint disable for console.warn in lib --- lib/src/Components/Auth/useAuth.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/Components/Auth/useAuth.tsx b/lib/src/Components/Auth/useAuth.tsx index be8ccc286..14cdbe0cf 100644 --- a/lib/src/Components/Auth/useAuth.tsx +++ b/lib/src/Components/Auth/useAuth.tsx @@ -69,6 +69,7 @@ export const AuthProvider = ({ userApi, children }: AuthProviderProps) => { } // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { + // eslint-disable-next-line no-console console.warn('Failed to load user token:', error) setLoading(false) return undefined From a8828ae521b8d2f9e89b898064a9b9e2a768ce1a Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Tue, 5 Aug 2025 09:33:06 +0200 Subject: [PATCH 3/6] fix: add localStorage error handling to theme functionality - Create safeLocalStorage utility for graceful error handling - Update useTheme hook to use safeLocalStorage - Update ThemeControl component to use safeLocalStorage - Prevents SecurityError when localStorage is unavailable in theme features This extends the localStorage fix from issue #212 to cover all localStorage usage in the codebase. --- .../Components/AppShell/hooks/useTheme.tsx | 6 ++-- lib/src/Components/Templates/ThemeControl.tsx | 10 ++++-- lib/src/Utils/localStorage.ts | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 lib/src/Utils/localStorage.ts diff --git a/lib/src/Components/AppShell/hooks/useTheme.tsx b/lib/src/Components/AppShell/hooks/useTheme.tsx index 367d8fd3d..254996b9f 100644 --- a/lib/src/Components/AppShell/hooks/useTheme.tsx +++ b/lib/src/Components/AppShell/hooks/useTheme.tsx @@ -1,12 +1,14 @@ import { useEffect } from 'react' +import { safeLocalStorage } from '#utils/localStorage' + export const useTheme = (defaultTheme = 'default') => { useEffect(() => { - const savedTheme = localStorage.getItem('theme') + const savedTheme = safeLocalStorage.getItem('theme') const initialTheme = savedTheme ? (JSON.parse(savedTheme) as string) : defaultTheme if (initialTheme !== 'default') { document.documentElement.setAttribute('data-theme', defaultTheme) - localStorage.setItem('theme', JSON.stringify(initialTheme)) + safeLocalStorage.setItem('theme', JSON.stringify(initialTheme)) } }, [defaultTheme]) } diff --git a/lib/src/Components/Templates/ThemeControl.tsx b/lib/src/Components/Templates/ThemeControl.tsx index 6ed593fcd..28cc2c7a5 100644 --- a/lib/src/Components/Templates/ThemeControl.tsx +++ b/lib/src/Components/Templates/ThemeControl.tsx @@ -1,5 +1,7 @@ import { useState, useEffect } from 'react' +import { safeLocalStorage } from '#utils/localStorage' + const themes = [ 'default', 'light', @@ -15,14 +17,16 @@ const themes = [ export const ThemeControl = () => { const [theme, setTheme] = useState(() => { - const savedTheme = localStorage.getItem('theme') + const savedTheme = safeLocalStorage.getItem('theme') return savedTheme ? (JSON.parse(savedTheme) as string) : 'default' }) useEffect(() => { if (theme !== 'default') { - localStorage.setItem('theme', JSON.stringify(theme)) - } else localStorage.removeItem('theme') + safeLocalStorage.setItem('theme', JSON.stringify(theme)) + } else { + safeLocalStorage.removeItem('theme') + } document.documentElement.setAttribute('data-theme', theme) }, [theme]) diff --git a/lib/src/Utils/localStorage.ts b/lib/src/Utils/localStorage.ts new file mode 100644 index 000000000..47dbc2d22 --- /dev/null +++ b/lib/src/Utils/localStorage.ts @@ -0,0 +1,32 @@ +/** + * Safe localStorage utility that handles SecurityError gracefully + * when localStorage is not available (e.g., private browsing mode) + */ +export const safeLocalStorage = { + getItem: (key: string): string | null => { + try { + return localStorage.getItem(key) + } catch (error) { + console.warn(`localStorage.getItem failed for key "${key}":`, error) + return null + } + }, + + setItem: (key: string, value: string): void => { + try { + localStorage.setItem(key, value) + } catch (error) { + console.warn(`localStorage.setItem failed for key "${key}":`, error) + // Silently fail - functionality will work in memory-only mode + } + }, + + removeItem: (key: string): void => { + try { + localStorage.removeItem(key) + } catch (error) { + console.warn(`localStorage.removeItem failed for key "${key}":`, error) + // Silently fail + } + }, +} \ No newline at end of file From 63f9129d53ac8f4c1ab9d9c77d0b2713b3efc002 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Tue, 5 Aug 2025 09:35:40 +0200 Subject: [PATCH 4/6] fix: add eslint disables for localStorage utility --- lib/src/Utils/localStorage.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/Utils/localStorage.ts b/lib/src/Utils/localStorage.ts index 47dbc2d22..57ff58ac1 100644 --- a/lib/src/Utils/localStorage.ts +++ b/lib/src/Utils/localStorage.ts @@ -1,3 +1,6 @@ +/* eslint-disable no-catch-all/no-catch-all */ +/* eslint-disable no-console */ + /** * Safe localStorage utility that handles SecurityError gracefully * when localStorage is not available (e.g., private browsing mode) From 5dad538577c518d6e6cd814b92b1ca47688ed16d Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Tue, 5 Aug 2025 09:47:52 +0200 Subject: [PATCH 5/6] fix: prettier formatting + add lint check hook - Fix prettier formatting for localStorage utility - Add pre-PR lint check script to prevent CI failures - Create Claude Code hook configuration for automatic linting --- lib/src/Utils/localStorage.ts | 2 +- scripts/check-lint.sh | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100755 scripts/check-lint.sh diff --git a/lib/src/Utils/localStorage.ts b/lib/src/Utils/localStorage.ts index 57ff58ac1..f3590493c 100644 --- a/lib/src/Utils/localStorage.ts +++ b/lib/src/Utils/localStorage.ts @@ -32,4 +32,4 @@ export const safeLocalStorage = { // Silently fail } }, -} \ No newline at end of file +} diff --git a/scripts/check-lint.sh b/scripts/check-lint.sh new file mode 100755 index 000000000..3007da939 --- /dev/null +++ b/scripts/check-lint.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Claude Code Hook: Pre-PR Lint Check +# This script runs linting checks on both app and lib before PR operations + +set -e # Exit on any error + +echo "🔍 Running lint checks before PR operation..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +LINT_FAILED=0 + +# Check if we're in the utopia-map directory +if [[ ! -f "tsconfig.base.json" ]]; then + echo -e "${RED}❌ Error: Must be run from utopia-map root directory${NC}" + exit 1 +fi + +echo -e "${YELLOW}📋 Checking app linting...${NC}" +cd app +if npm run test:lint:eslint; then + echo -e "${GREEN}✅ App linting passed${NC}" +else + echo -e "${RED}❌ App linting failed${NC}" + LINT_FAILED=1 +fi +cd .. + +echo -e "${YELLOW}📋 Checking lib linting...${NC}" +cd lib +if npm run test:lint:eslint; then + echo -e "${GREEN}✅ Lib linting passed${NC}" +else + echo -e "${RED}❌ Lib linting failed${NC}" + LINT_FAILED=1 +fi +cd .. + +if [[ $LINT_FAILED -eq 1 ]]; then + echo -e "${RED}❌ Lint checks failed. Please fix linting errors before creating/updating PR.${NC}" + echo -e "${YELLOW}💡 Tip: Run 'npm run lintfix' in the failing directory to auto-fix some issues.${NC}" + exit 1 +else + echo -e "${GREEN}🎉 All lint checks passed! Ready for PR operation.${NC}" +fi \ No newline at end of file From b0a8a14efe002ecee1db02ce4342472f01fde4f9 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Tue, 5 Aug 2025 09:53:03 +0200 Subject: [PATCH 6/6] cleanup: remove infrastructure files from feature branch - Remove check-lint.sh script (belongs in claude branch) - Remove CLAUDE.md and other infrastructure files - Keep feature branch focused on localStorage fixes only --- scripts/check-lint.sh | 50 ------------------------------------------- 1 file changed, 50 deletions(-) delete mode 100755 scripts/check-lint.sh diff --git a/scripts/check-lint.sh b/scripts/check-lint.sh deleted file mode 100755 index 3007da939..000000000 --- a/scripts/check-lint.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# Claude Code Hook: Pre-PR Lint Check -# This script runs linting checks on both app and lib before PR operations - -set -e # Exit on any error - -echo "🔍 Running lint checks before PR operation..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -LINT_FAILED=0 - -# Check if we're in the utopia-map directory -if [[ ! -f "tsconfig.base.json" ]]; then - echo -e "${RED}❌ Error: Must be run from utopia-map root directory${NC}" - exit 1 -fi - -echo -e "${YELLOW}📋 Checking app linting...${NC}" -cd app -if npm run test:lint:eslint; then - echo -e "${GREEN}✅ App linting passed${NC}" -else - echo -e "${RED}❌ App linting failed${NC}" - LINT_FAILED=1 -fi -cd .. - -echo -e "${YELLOW}📋 Checking lib linting...${NC}" -cd lib -if npm run test:lint:eslint; then - echo -e "${GREEN}✅ Lib linting passed${NC}" -else - echo -e "${RED}❌ Lib linting failed${NC}" - LINT_FAILED=1 -fi -cd .. - -if [[ $LINT_FAILED -eq 1 ]]; then - echo -e "${RED}❌ Lint checks failed. Please fix linting errors before creating/updating PR.${NC}" - echo -e "${YELLOW}💡 Tip: Run 'npm run lintfix' in the failing directory to auto-fix some issues.${NC}" - exit 1 -else - echo -e "${GREEN}🎉 All lint checks passed! Ready for PR operation.${NC}" -fi \ No newline at end of file