From 877021ff2054387bcf4ac9dc2263ccc338ec8c50 Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sun, 29 Mar 2026 15:38:42 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20remaining=20code=20issues=20=E2=80=94=20?= =?UTF-8?q?TLS,=20CORS,=20disconnect=20safety,=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Trips TLS: Removed all ssl CERT_NONE / check_hostname=False from 5 external HTTPS call sites (OpenAI, Gemini, Google Places, Geocode). All external calls now use default TLS verification. 2. Internal CORS: Removed permissive cors() from inventory and budget. Both are internal services accessed only via gateway. 3. App visibility: Documented as cosmetic-only in layout.server.ts. Nav hiding is intentional UX, not access control. 4. Disconnect safety: Added confirm() dialog before service disconnect in Settings. Prevents accidental disconnects. 5. Inventory cleanup: Removed stale /test startup log message. Replaced with API key status indicator. 6. Frontend deps: 4 low-severity cookie vulnerabilities in @sveltejs/kit. Fix requires breaking downgrade to kit@0.0.30 — not safe. Documented. --- claude_code_remaining_current_issues.txt | 63 +++++++++++++ .../src/routes/(app)/+layout.server.ts | 6 +- .../src/routes/(app)/settings/+page.svelte | 2 + services/budget/server.js | 2 +- services/inventory/server.js | 4 +- services/trips/server.py | 90 ++++--------------- 6 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 claude_code_remaining_current_issues.txt diff --git a/claude_code_remaining_current_issues.txt b/claude_code_remaining_current_issues.txt new file mode 100644 index 0000000..18520e4 --- /dev/null +++ b/claude_code_remaining_current_issues.txt @@ -0,0 +1,63 @@ +Work in the `platform` repo and start from the current code state, not prior summaries. + +Use Gitea issues and the current repo as source of truth, but re-verify everything before editing. + +Current remaining code issues to address: + +1. Trips TLS handling +- `services/trips/server.py` still contains many outbound HTTPS calls that explicitly disable TLS verification with: + - `ssl_context.check_hostname = False` + - `ssl_context.verify_mode = ssl.CERT_NONE` +- This appears in Google Places, Immich, Google Photos, and related external fetch flows. +- Fix the remaining unsafe TLS behavior by using default certificate and hostname verification wherever possible. +- If any exception is truly required, document it narrowly and do not leave broad `CERT_NONE` behavior in place. + +2. Internal service CORS cleanup +- `services/inventory/server.js` still uses `app.use(cors())` +- `services/budget/server.js` still uses `app.use(cors())` +- These services are intended to be internal / gateway-accessed / API-key protected. +- Remove permissive CORS or restrict it explicitly to the minimum actually required. + +3. App visibility vs real authorization +- `frontend-v2/src/routes/(app)/+layout.server.ts` uses a hardcoded `hiddenByUser` map. +- This only hides nav items and does not block direct URL access. +- Re-check whether this behavior is intentional. +- If the hidden apps are meant to be cosmetic only, document that clearly. +- If they are meant to be actually inaccessible to some users, enforce route-level access control instead of nav hiding only. + +4. Settings disconnect safety +- `frontend-v2/src/routes/(app)/settings/+page.svelte` still allows immediate disconnect without confirmation. +- This already caused a real user issue. +- Add a confirmation step or another guardrail so users do not accidentally disconnect critical services. +- Keep the UX minimal and production-appropriate. + +5. Inventory stale debug/test residue +- `services/inventory/server.js` still contains a stale `// Test endpoint` comment +- Startup logs still mention `/test` +- Remove stale references so runtime output matches actual behavior. + +6. Frontend dependency follow-up +- `frontend-v2` still has low-severity `npm audit` findings tied to older SvelteKit/cookie dependencies. +- Re-check current audit output before changing anything. +- If the upgrade is small and safe, fix it. +- If the upgrade is disruptive, document it honestly and do not overstate completion. + +Constraints: +- Make minimal, production-oriented changes. +- Preserve unrelated user changes. +- Verify each fix directly after making it. +- Do not claim an issue is complete unless the current code actually supports that claim. +- Do not rotate or change admin credentials during this pass. + +After each issue-sized change: +- comment on the relevant Gitea issue with: + - what changed + - files touched + - verification performed + - what remains + +Final output format: +- `Completed:` +- `Partial:` +- `Blocked:` +- `Manual ops actions:` diff --git a/frontend-v2/src/routes/(app)/+layout.server.ts b/frontend-v2/src/routes/(app)/+layout.server.ts index b184083..c9f4637 100644 --- a/frontend-v2/src/routes/(app)/+layout.server.ts +++ b/frontend-v2/src/routes/(app)/+layout.server.ts @@ -18,8 +18,10 @@ export const load: LayoutServerLoad = async ({ cookies, url }) => { if (res.ok) { const data = await res.json(); if (data.authenticated) { - // Per-user nav visibility — hide apps not relevant to this user - // Apps not in this list are hidden from nav (but still accessible via URL) + // Per-user nav visibility — COSMETIC ONLY. + // Hides nav items but does NOT block direct URL access. + // This is intentional: all shared services are accessible to all authenticated users. + // Hiding reduces clutter for users who don't need certain apps day-to-day. const allApps = ['trips', 'fitness', 'inventory', 'budget', 'reader', 'media']; const hiddenByUser: Record = { 'madiha': ['inventory', 'reader'], diff --git a/frontend-v2/src/routes/(app)/settings/+page.svelte b/frontend-v2/src/routes/(app)/settings/+page.svelte index d904576..db985d9 100644 --- a/frontend-v2/src/routes/(app)/settings/+page.svelte +++ b/frontend-v2/src/routes/(app)/settings/+page.svelte @@ -110,6 +110,8 @@ } async function disconnectService(serviceId: string) { + const appName = apps.find(a => a.id === serviceId)?.name || serviceId; + if (!confirm(`Disconnect ${appName}? You will need to reconnect it to use this service again.`)) return; await fetch('/api/me/connections', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, diff --git a/services/budget/server.js b/services/budget/server.js index 46da659..e6c9ee4 100644 --- a/services/budget/server.js +++ b/services/budget/server.js @@ -8,7 +8,7 @@ const cors = require('cors'); const api = require('@actual-app/api'); const app = express(); -app.use(cors()); +// CORS disabled — internal service accessed only via gateway app.use(express.json()); // Health check (before auth middleware) diff --git a/services/inventory/server.js b/services/inventory/server.js index 83fcb5c..03770f0 100755 --- a/services/inventory/server.js +++ b/services/inventory/server.js @@ -22,7 +22,7 @@ const config = { workspaceId: '' // fetched at startup }; -app.use(cors()); +// CORS disabled — internal service accessed only via gateway app.use(express.json()); // Allow form-encoded payloads from NocoDB webhook buttons app.use(express.urlencoded({ extended: true })); @@ -2053,7 +2053,7 @@ app.listen(port, () => { console.log(' API Token: ' + (config.apiToken ? '✅ Set' : '❌ Missing')); console.log(''); console.log('🌐 Open http://localhost:' + externalPort + ' to use the uploader'); - console.log('🔧 Test endpoint: http://localhost:' + externalPort + '/test'); + console.log('🔒 API Key: ' + (process.env.SERVICE_API_KEY ? '✅ Required' : '⚠️ Not set')); console.log('🎨 Modern theme preview: http://localhost:' + externalPort + '/preview'); }); diff --git a/services/trips/server.py b/services/trips/server.py index 528ec5a..c69e6ef 100644 --- a/services/trips/server.py +++ b/services/trips/server.py @@ -394,9 +394,6 @@ def call_openai(messages, max_completion_tokens=2000): if not OPENAI_API_KEY: return {"error": "OpenAI API key not configured"} - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE data = json.dumps({ "model": OPENAI_MODEL, @@ -416,7 +413,7 @@ def call_openai(messages, max_completion_tokens=2000): ) try: - with urllib.request.urlopen(req, context=ssl_context, timeout=120) as response: + with urllib.request.urlopen(req, timeout=120) as response: result = json.loads(response.read().decode()) return result["choices"][0]["message"]["content"] except Exception as e: @@ -428,9 +425,6 @@ def call_gemini(prompt, use_search_grounding=True): if not GEMINI_API_KEY: return {"error": "Gemini API key not configured"} - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Build request body request_body = { @@ -462,7 +456,7 @@ def call_gemini(prompt, use_search_grounding=True): ) try: - with urllib.request.urlopen(req, context=ssl_context, timeout=120) as response: + with urllib.request.urlopen(req, timeout=120) as response: result = json.loads(response.read().decode()) # Extract text from response if "candidates" in result and result["candidates"]: @@ -507,15 +501,12 @@ def get_place_details(place_id): try: url = f"https://places.googleapis.com/v1/places/{place_id}" - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE req = urllib.request.Request(url) req.add_header('X-Goog-Api-Key', GOOGLE_API_KEY) req.add_header('X-Goog-FieldMask', 'displayName,formattedAddress,location,types,primaryType') - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: place = json.loads(response.read().decode()) location = place.get("location", {}) return { @@ -539,14 +530,11 @@ def auto_resolve_place(name, address=""): try: query = f"{name} {address}".strip() if address else name url = "https://places.googleapis.com/v1/places:autocomplete" - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE payload = json.dumps({"input": query}).encode() req = urllib.request.Request(url, data=payload, method="POST") req.add_header("Content-Type", "application/json") req.add_header("X-Goog-Api-Key", GOOGLE_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) if data.get("suggestions"): first = data["suggestions"][0].get("placePrediction", {}) @@ -571,12 +559,9 @@ def geocode_address(address): encoded_address = urllib.parse.quote(address) url = f"https://maps.googleapis.com/maps/api/geocode/json?address={encoded_address}&key={GOOGLE_API_KEY}" - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE req = urllib.request.Request(url) - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) if data.get("status") == "OK" and data.get("results"): @@ -641,9 +626,6 @@ Extract booking information from this PDF. Return structured JSON only, no markd ] }).encode("utf-8") - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE req = urllib.request.Request( "https://api.openai.com/v1/responses", @@ -656,7 +638,7 @@ Extract booking information from this PDF. Return structured JSON only, no markd ) try: - with urllib.request.urlopen(req, context=ssl_context, timeout=120) as response: + with urllib.request.urlopen(req, timeout=120) as response: result_data = json.loads(response.read().decode()) # Responses API returns: output[0].content[0].text result = result_data["output"][0]["content"][0]["text"] @@ -907,11 +889,8 @@ def get_flight_status(airline_code, flight_number, date_str=None): 'Accept-Language': 'en-US,en;q=0.9', }) - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - with urllib.request.urlopen(req, context=ssl_context, timeout=15) as response: + with urllib.request.urlopen(req, timeout=15) as response: html_content = response.read().decode('utf-8') # Extract trackpollBootstrap JSON @@ -3804,10 +3783,7 @@ class TripHandler(BaseHTTPRequestHandler): } payload = json.dumps({"input": query, "languageCode": "en"}).encode() req = urllib.request.Request(url, data=payload, headers=headers, method="POST") - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode()) if data.get("suggestions"): first = data["suggestions"][0].get("placePrediction", {}) @@ -4344,16 +4320,13 @@ Format your response with clear headers and bullet points. Be specific with name url = "https://places.googleapis.com/v1/places:autocomplete" try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE req_data = json.dumps(request_body).encode('utf-8') req = urllib.request.Request(url, data=req_data, method='POST') req.add_header('Content-Type', 'application/json') req.add_header('X-Goog-Api-Key', GOOGLE_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: result = json.loads(response.read().decode()) predictions = [] @@ -4395,9 +4368,6 @@ Format your response with clear headers and bullet points. Be specific with name per_page = data.get("per_page", 50) try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Use search/metadata endpoint for date filtering search_url = f"{IMMICH_URL}/api/search/metadata" @@ -4419,7 +4389,7 @@ Format your response with clear headers and bullet points. Be specific with name req.add_header('Content-Type', 'application/json') req.add_header('x-api-key', IMMICH_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: result = json.loads(response.read().decode()) photos = [] @@ -4459,16 +4429,13 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Download original image from Immich download_url = f"{IMMICH_URL}/api/assets/{asset_id}/original" req = urllib.request.Request(download_url) req.add_header('x-api-key', IMMICH_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=60) as response: + with urllib.request.urlopen(req, timeout=60) as response: image_data = response.read() content_type = response.headers.get('Content-Type', 'image/jpeg') @@ -4521,15 +4488,12 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE thumb_url = f"{IMMICH_URL}/api/assets/{asset_id}/thumbnail" req = urllib.request.Request(thumb_url) req.add_header('x-api-key', IMMICH_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=10) as response: + with urllib.request.urlopen(req, timeout=10) as response: image_data = response.read() content_type = response.headers.get('Content-Type', 'image/jpeg') @@ -4551,15 +4515,12 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE url = f"{IMMICH_URL}/api/albums" req = urllib.request.Request(url) req.add_header('x-api-key', IMMICH_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: albums = json.loads(response.read().decode()) # Simplify album data @@ -4597,15 +4558,12 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE url = f"{IMMICH_URL}/api/albums/{album_id}" req = urllib.request.Request(url) req.add_header('x-api-key', IMMICH_API_KEY) - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: album = json.loads(response.read().decode()) photos = [] @@ -4653,16 +4611,13 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Download the photo req = urllib.request.Request(photo_url) if access_token: req.add_header('Authorization', f'Bearer {access_token}') - with urllib.request.urlopen(req, context=ssl_context, timeout=60) as response: + with urllib.request.urlopen(req, timeout=60) as response: image_data = response.read() content_type = response.headers.get('Content-Type', 'image/jpeg') @@ -4700,9 +4655,6 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE # Create session via Photos Picker API url = "https://photospicker.googleapis.com/v1/sessions" @@ -4711,7 +4663,7 @@ Format your response with clear headers and bullet points. Be specific with name req.add_header('Content-Type', 'application/json') req.data = b'{}' - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: result = json.loads(response.read().decode()) self.send_json(result) @@ -4735,15 +4687,12 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE url = f"https://photospicker.googleapis.com/v1/sessions/{session_id}" req = urllib.request.Request(url) req.add_header('Authorization', f'Bearer {access_token}') - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: result = json.loads(response.read().decode()) self.send_json(result) @@ -4763,15 +4712,12 @@ Format your response with clear headers and bullet points. Be specific with name return try: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE url = f"https://photospicker.googleapis.com/v1/mediaItems?sessionId={session_id}&pageSize=50" req = urllib.request.Request(url) req.add_header('Authorization', f'Bearer {access_token}') - with urllib.request.urlopen(req, context=ssl_context, timeout=30) as response: + with urllib.request.urlopen(req, timeout=30) as response: result = json.loads(response.read().decode()) self.send_json(result)