From c1df3b5aa312fa5add986407598db6a70cb043d3 Mon Sep 17 00:00:00 2001 From: bonamin Date: Wed, 30 Sep 2026 15:31:34 +0300 Subject: [PATCH] fix(users): console-created users use the Firebase uid as doc ID create_user wrote the profile with .add() (random doc ID). On first login the FlutterFlow app looks for users/{uid}, doesn't find it, and creates a second, bare doc - so every console-created user ended up duplicated, and devices assigned in the console pointed at the doc the app never reads. Now the profile is written to users/{uid} with created_time set, and the email is lowercased to match what Firebase Auth stores. If the Firestore write fails, the just-created Auth account is deleted so no orphan is left. Co-Authored-By: Claude Opus 5.5 --- backend/users/service.py | 29 ++++++++++++++++++++++------- docs/mqtt-app-user-auth.md | 4 +++- 2 files changed, 25 insertions(+), 8 deletions(-) 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