feat(auth): harden sessions + HA Ingress support
CI / backend (pytest + alembic) (push) Has been cancelled
CI / frontend (build) (push) Has been cancelled

- backend: settings SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH,
  require_session uses secrets.compare_digest and respects trusted-network
  opt-in, main.py adds require_family_session middleware gating all /api/
  routes except auth/admin/email-vote-token paths
- docker-compose: pass SESSION_COOKIE_SECURE + TRUSTED_NETWORK_AUTO_AUTH
  through to backend + scheduler (fixes env-file changes not reaching runtime)
- frontend: Ingress path-prefix support (APP_BASE_PATH, BrowserRouter basename,
  vite base './'), Login redirect honors APP_BASE_PATH
- nginx: no-cache headers on root + /assets/
- docs: Home Assistant Ingress install/troubleshooting + plan file
- tests: test_auth expects 401 on no-session GET

Defaults: SESSION_COOKIE_SECURE=false, TRUSTED_NETWORK_AUTO_AUTH=true
(HA is the auth boundary; MealPlanner must not be port-forwarded directly).
This commit is contained in:
2026-06-30 16:11:33 -07:00
parent 7f5757094e
commit 7838c49721
14 changed files with 148 additions and 38 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ import { OnboardingTour, useOnboarding } from './components/OnboardingTour'
import { showApiError } from './lib/toast'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { requestFocusSearch } from './hooks/useFocusSearch'
import { APP_BASE_PATH } from './api'
import Dashboard from './pages/Dashboard'
import MealDetail from './pages/MealDetail'
import Pantry from './pages/Pantry'
@@ -81,7 +82,7 @@ function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<BrowserRouter basename={APP_BASE_PATH || undefined}>
<GlobalShortcuts />
<div className="min-h-screen bg-surface-50">
<Navigation />
+10 -1
View File
@@ -1,6 +1,15 @@
import axios from 'axios'
const API_BASE = import.meta.env.VITE_API_URL || '/api'
export function getIngressBasePath() {
const parts = window.location.pathname.split('/').filter(Boolean)
if (parts[0] === 'api' && parts[1] === 'hassio_ingress' && parts[2]) {
return `/${parts.slice(0, 3).join('/')}`
}
return ''
}
export const APP_BASE_PATH = getIngressBasePath()
const API_BASE = import.meta.env.VITE_API_URL || `${APP_BASE_PATH}/api`
const api = axios.create({
baseURL: API_BASE,
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { Lock, ArrowRight } from 'lucide-react'
import { mealPlannerApi } from '../api'
import { APP_BASE_PATH, mealPlannerApi } from '../api'
import { Button } from '../components/ui/Button'
import { Card, CardBody } from '../components/ui/Card'
import { Input } from '../components/ui/Input'
@@ -16,7 +16,7 @@ export default function Login() {
setLoading(true)
try {
await mealPlannerApi.auth.login(password)
window.location.href = '/'
window.location.href = `${APP_BASE_PATH}/`
} catch {
setError('Incorrect password. Try again.')
} finally {
+1
View File
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './',
server: {
port: 3000,
proxy: {