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 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user