diff --git a/backend/users/service.py b/backend/users/service.py index a1e426c..e76df8d 100644 --- a/backend/users/service.py +++ b/backend/users/service.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from google.cloud.firestore_v1 import DocumentReference, ArrayUnion, ArrayRemove @@ -116,30 +116,45 @@ def get_user(user_doc_id: str) -> UserInDB: def create_user(data: UserCreate) -> UserInDB: """Create a new user: a Firebase Auth account (so they can log into the app - immediately) plus the matching Firestore profile document.""" + immediately) plus the matching Firestore profile document. + + The profile doc ID MUST be the Firebase Auth uid. The FlutterFlow app looks + up `users/{uid}` on login and creates a bare doc there if it's missing, and + MQTT auth (mqtt/app_users.py) + device `user_list` paths assume the same. + """ if not data.password or len(data.password) < 6: raise ValidationError("Password must be at least 6 characters.") - if not data.email: + # Firebase Auth lowercases emails; store the same form so the profile + # matches what the app sees for the signed-in user. + email = (data.email or "").strip().lower() + if not email: raise ValidationError("Email is required.") db = get_db() doc_data = data.model_dump(exclude={"password"}) + doc_data["email"] = email doc_data["friendsList"] = [] doc_data["friendsInvited"] = [] + doc_data["created_time"] = datetime.now(timezone.utc) try: firebase_user = firebase_auth.create_user( - email=data.email, + email=email, password=data.password, display_name=data.display_name or None, ) except firebase_auth.EmailAlreadyExistsError: - raise ValidationError(f"A user with email {data.email} already exists.") + raise ValidationError(f"A user with email {email} already exists.") doc_data["uid"] = firebase_user.uid - _, doc_ref = db.collection(COLLECTION).add(doc_data) + try: + db.collection(COLLECTION).document(firebase_user.uid).set(doc_data) + except Exception: + # Don't leave an Auth account behind with no profile doc. + firebase_auth.delete_user(firebase_user.uid) + raise - return UserInDB(id=doc_ref.id, **doc_data) + return UserInDB(id=firebase_user.uid, **_sanitize_dict(doc_data)) def update_user(user_doc_id: str, data: UserUpdate) -> UserInDB: diff --git a/docs/mqtt-app-user-auth.md b/docs/mqtt-app-user-auth.md index 874ac67..296cee5 100644 --- a/docs/mqtt-app-user-auth.md +++ b/docs/mqtt-app-user-auth.md @@ -64,7 +64,9 @@ flashed into NVS and the firmware uses it as its MQTT id. Legacy docs only have - `PUT /api/devices/{id}` when the body contains `user_list` (`devices.service.update_device`) - Users are resolved by the **`uid` field** (a query), **never by doc ID**. Console- - created users get random doc IDs (`.add()`); FlutterFlow uses the uid as the doc ID. + created users used to get random doc IDs (`.add()`) — fixed 2026-09-30, the Console + now writes `users/{uid}` like FlutterFlow does — but older docs may still have a + random ID, so keep resolving by the `uid` field. - A user is refused when `status == "blocked"`. - Lookups are cached in-process for 60 s (`mqtt/app_users.py`). Assign/unassign, update, block/unblock and delete call `invalidate(uid)`. If you add a new code path