Public Access
Merge branch 'feature/mvp-login'
This commit is contained in:
@@ -4,6 +4,8 @@ import Dashboard from './pages/Dashboard'
|
|||||||
import MealDetail from './pages/MealDetail'
|
import MealDetail from './pages/MealDetail'
|
||||||
import Pantry from './pages/Pantry'
|
import Pantry from './pages/Pantry'
|
||||||
import ShoppingList from './pages/ShoppingList'
|
import ShoppingList from './pages/ShoppingList'
|
||||||
|
import Login from './pages/Login'
|
||||||
|
import { mealPlannerApi } from './api'
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
@@ -26,6 +28,17 @@ function App() {
|
|||||||
Shopping List
|
Shopping List
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await mealPlannerApi.auth.logout().catch(() => {})
|
||||||
|
window.location.href = '/login'
|
||||||
|
}}
|
||||||
|
className="text-sm text-gray-500 hover:text-gray-900"
|
||||||
|
>
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -35,6 +48,7 @@ function App() {
|
|||||||
<Route path="/meals/:id" element={<MealDetail />} />
|
<Route path="/meals/:id" element={<MealDetail />} />
|
||||||
<Route path="/pantry" element={<Pantry />} />
|
<Route path="/pantry" element={<Pantry />} />
|
||||||
<Route path="/shopping-list" element={<ShoppingList />} />
|
<Route path="/shopping-list" element={<ShoppingList />} />
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ const api = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => {
|
||||||
|
if (error.response?.status === 401 && window.location.pathname !== '/login') {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const mealPlannerApi = {
|
export const mealPlannerApi = {
|
||||||
auth: {
|
auth: {
|
||||||
login: (password: string) => api.post('/auth/login', { password }),
|
login: (password: string) => api.post('/auth/login', { password }),
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { mealPlannerApi } from '../api'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await mealPlannerApi.auth.login(password)
|
||||||
|
window.location.href = '/'
|
||||||
|
} catch {
|
||||||
|
setError('Incorrect password. Try again.')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="bg-white rounded-lg shadow p-8 w-full max-w-sm">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-2 text-center">MealPlanner</h1>
|
||||||
|
<p className="text-sm text-gray-500 text-center mb-6">Family login</p>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className="text-red-600 text-sm">{error}</p>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 font-medium"
|
||||||
|
>
|
||||||
|
{loading ? 'Signing in…' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
One-time seed: creates the family profile + members.
|
||||||
|
|
||||||
|
Edit the MEMBERS list below to set real names and emails, then run:
|
||||||
|
|
||||||
|
docker cp scripts/seed_family.py mealplanner-backend-1:/app/seed_family.py
|
||||||
|
docker compose --env-file .env.test exec backend python /app/seed_family.py
|
||||||
|
|
||||||
|
Safe to re-run — exits early if a profile already exists.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ── Edit before running ───────────────────────────────────────────────────────
|
||||||
|
FAMILY_NAME = "Woolery"
|
||||||
|
FAMILY_CALORIE_TARGET = 500 # per serving per meal
|
||||||
|
HOUSEHOLD_SIZE = 4
|
||||||
|
ADULT_COUNT = 2
|
||||||
|
CHILD_COUNT = 2
|
||||||
|
|
||||||
|
MEMBERS = [
|
||||||
|
{"name": "Peter", "email": "peter@research.bike", "role": "adult", "likes_mushrooms": True},
|
||||||
|
{"name": "Wife", "email": "wife@example.com", "role": "adult", "likes_mushrooms": False},
|
||||||
|
{"name": "Kid 1", "email": "", "role": "child", "likes_mushrooms": False},
|
||||||
|
{"name": "Kid 2", "email": "", "role": "child", "likes_mushrooms": False},
|
||||||
|
]
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
sys.path.insert(0, "/app")
|
||||||
|
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.models import FamilyProfile, FamilyMember, FamilyMemberRole
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
existing = db.query(FamilyProfile).first()
|
||||||
|
if existing:
|
||||||
|
print(f"Family profile already exists (id={existing.id}). Nothing to do.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
profile = FamilyProfile(
|
||||||
|
name=FAMILY_NAME,
|
||||||
|
household_size=HOUSEHOLD_SIZE,
|
||||||
|
adult_count=ADULT_COUNT,
|
||||||
|
child_count=CHILD_COUNT,
|
||||||
|
calorie_target=FAMILY_CALORIE_TARGET,
|
||||||
|
)
|
||||||
|
db.add(profile)
|
||||||
|
db.flush()
|
||||||
|
print(f"Created family profile '{FAMILY_NAME}' id={profile.id}")
|
||||||
|
|
||||||
|
for m in MEMBERS:
|
||||||
|
if not m["email"]:
|
||||||
|
print(f" Skipping {m['name']} — no email set (edit MEMBERS to add one)")
|
||||||
|
continue
|
||||||
|
member = FamilyMember(
|
||||||
|
family_profile_id=profile.id,
|
||||||
|
name=m["name"],
|
||||||
|
email=m["email"],
|
||||||
|
role=FamilyMemberRole[m["role"].upper()],
|
||||||
|
likes_mushrooms=m["likes_mushrooms"],
|
||||||
|
)
|
||||||
|
db.add(member)
|
||||||
|
print(f" Added: {m['name']} <{m['email']}> role={m['role']}")
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print("Done.")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
Reference in New Issue
Block a user