phase complete
This commit is contained in:
parent
fb90a4ad32
commit
126ed0572c
8 changed files with 41 additions and 26 deletions
|
|
@ -1,2 +1,2 @@
|
|||
# API package for FastAPI routers.
|
||||
# Routers will be split by feature: recipes, meals, persons, shopping, auth.
|
||||
# Routers are split by feature: recipes, meals, shopping, auth, households.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import aiosqlite
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
|
||||
from api.deps import get_current_user, get_db, error_response, get_household_from_slug
|
||||
from pydantic import Field
|
||||
from common import ApiModel
|
||||
from users.models import User
|
||||
|
||||
|
|
@ -126,7 +127,12 @@ class InvitationResponse(ApiModel):
|
|||
status: str = "pending"
|
||||
|
||||
|
||||
@scoped.post("/invitations", response_model=InvitationResponse)
|
||||
class InviteLinkResponse(ApiModel):
|
||||
# Force snake_case in JSON output to match spec and tests
|
||||
invite_link: str = Field(serialization_alias="invite_link")
|
||||
|
||||
|
||||
@scoped.post("/invitations", response_model=InviteLinkResponse)
|
||||
async def create_invitation(
|
||||
request: Request,
|
||||
body: CreateInvitationBody,
|
||||
|
|
@ -136,6 +142,8 @@ async def create_invitation(
|
|||
):
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import urljoin, urlencode
|
||||
from settings import settings
|
||||
|
||||
token = secrets.token_urlsafe(24)
|
||||
expires_at = (datetime.utcnow() + timedelta(days=14)).isoformat() + "Z"
|
||||
|
|
@ -147,7 +155,13 @@ async def create_invitation(
|
|||
""",
|
||||
(household["id"], body.email, user.id, token, expires_at, "pending"),
|
||||
)
|
||||
return InvitationResponse(token=token)
|
||||
base = settings.frontend_dev_url or "http://localhost:8080/"
|
||||
# Ensure base ends with a slash for urljoin
|
||||
if not base.endswith("/"):
|
||||
base = base + "/"
|
||||
path_with_query = f"invitations/accept?{urlencode({'token': token})}"
|
||||
invite_link = urljoin(base, path_with_query)
|
||||
return InviteLinkResponse(invite_link=invite_link)
|
||||
except Exception:
|
||||
return error_response(request, 400, "Unable to create invitation")
|
||||
|
||||
|
|
|
|||
|
|
@ -440,13 +440,9 @@ async def delete_meal_scoped(
|
|||
if not meal:
|
||||
return error_response(request, 404, "Meal not found")
|
||||
# Remove outstanding requests for this meal in current household
|
||||
try:
|
||||
from shopping.repository import remove_meal_request_scoped
|
||||
|
||||
await remove_meal_request_scoped(conn, meal_id, hid)
|
||||
except Exception:
|
||||
# Fallback: remove regardless of household (legacy cleanup)
|
||||
await shopping.remove_request(conn, person=None, meal=meal)
|
||||
await meals.delete_meal(conn, meal.id)
|
||||
|
||||
return MealOut(
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ The objective is to complete the final remaining tasks to officially close out t
|
|||
|
||||
This checklist represents all remaining work.
|
||||
|
||||
- [ ] **1. Implement "Copy Invite Link" API**:
|
||||
- [x] **1. Implement "Copy Invite Link" API**:
|
||||
- **Objective**: Modify the invitation creation logic to support a "copy link" UX on the frontend, instead of sending an email.
|
||||
- **File**: `api/households.py`
|
||||
- **Action**: The existing `POST /api/v1/households/{householdSlug}/invitations` endpoint should be modified. Instead of returning a simple `201 Created`, it must create the invitation token and return a JSON object containing the full, shareable URL.
|
||||
|
|
@ -21,11 +21,11 @@ This checklist represents all remaining work.
|
|||
}
|
||||
```
|
||||
|
||||
- [ ] **2. Final Codebase Sweep**:
|
||||
- [x] **2. Final Codebase Sweep**:
|
||||
- **Objective**: Perform a final search for and remove any dead code, comments, or variables related to the old system.
|
||||
- **Action**: Search the entire codebase for the following keywords: `legacy`, `old`, `previous`, `workaround`, `fallback`, `person`.
|
||||
- **Outcome**: Any remaining artifacts from the migration are pruned, leaving the codebase in a clean, maintainable state for future development.
|
||||
|
||||
- [ ] **3. Mark Project as Complete**:
|
||||
- [x] **3. Mark Project as Complete**:
|
||||
- **Objective**: Once the above tasks are done, this document is complete.
|
||||
- **Action**: Check this box and archive this specification.
|
||||
|
|
|
|||
2
main.py
2
main.py
|
|
@ -89,7 +89,7 @@ async def request_validation_exc_handler(request: Request, exc: Exception):
|
|||
for e in exc.errors():
|
||||
loc = ".".join([str(p) for p in e.get("loc", [])])
|
||||
errors.setdefault(loc, []).append(e.get("msg"))
|
||||
# Legacy cookie-based auth behavior removed; standard 422 for validation errors
|
||||
# Standard 422 for request validation errors
|
||||
|
||||
body = ProblemDetails(
|
||||
title="Validation Error",
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ async def bulk_load_participants(conn, meals: List[Meal]) -> None:
|
|||
"""Populate participants for many meals in one query to avoid N+1.
|
||||
|
||||
For each meal, fills meal.chefs, meal.cleanup, meal.consumers using a bulk
|
||||
lookup of MealParticipant rows and a single persons.get_by_ids fetch.
|
||||
lookup of MealParticipant rows and a single user lookup.
|
||||
"""
|
||||
if not meals:
|
||||
return
|
||||
|
|
|
|||
17
openapi.json
17
openapi.json
|
|
@ -397,7 +397,7 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvitationResponse"
|
||||
"$ref": "#/components/schemas/InviteLinkResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2078,23 +2078,18 @@
|
|||
],
|
||||
"title": "IngredientPurchaseItemIn"
|
||||
},
|
||||
"InvitationResponse": {
|
||||
"InviteLinkResponse": {
|
||||
"properties": {
|
||||
"token": {
|
||||
"invite_link": {
|
||||
"type": "string",
|
||||
"title": "Token"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"title": "Status",
|
||||
"default": "pending"
|
||||
"title": "Invite Link"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"token"
|
||||
"invite_link"
|
||||
],
|
||||
"title": "InvitationResponse"
|
||||
"title": "InviteLinkResponse"
|
||||
},
|
||||
"ListIngredientItem": {
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,17 @@ class TestInvitationsV2(unittest.IsolatedAsyncioTestCase):
|
|||
json={"email": "invitee@test.com"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
token = r.json()["token"]
|
||||
body = r.json()
|
||||
invite_link = body.get("invite_link")
|
||||
assert isinstance(invite_link, str) and "/invitations/accept?token=" in invite_link
|
||||
|
||||
# Extract token from invite_link
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
qs = parse_qs(urlparse(invite_link).query)
|
||||
token_list = qs.get("token", [])
|
||||
assert token_list and isinstance(token_list[0], str)
|
||||
token = token_list[0]
|
||||
assert isinstance(token, str) and len(token) >= 16
|
||||
|
||||
# Register invitee
|
||||
|
|
|
|||
Loading…
Reference in a new issue