From 0d79a9f72c7d22c5584231b100f6e5f81bf32e28 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 21 Sep 2026 18:31:37 +0300 Subject: [PATCH] feat(users): support creating app users with a Firebase Auth account New users can now be created with a password, which creates a real Firebase Auth account (so they can log into the mobile app immediately) alongside the Firestore profile document. UserCreate is now UserProfile + password (request-only, never persisted or echoed back); deleting a user also removes their Auth account. - backend/users: split UserCreate into UserProfile (persisted shape) and UserCreate (adds password), wire firebase_auth create/delete - CreateUserModal: new lightweight modal for creating a user from other flows (e.g. device onboarding) without leaving the page - UserForm: adds the password field for new users; also fixes useToast() being used undestructured (toast.success(...) was being called on the hook's return value instead of its .toast method) Co-Authored-By: Claude Sonnet 5 --- backend/users/models.py | 8 +- backend/users/service.py | 29 +++++- .../bellcloud/users/CreateUserModal.jsx | 97 +++++++++++++++++++ .../src/pages/bellcloud/users/UserForm.jsx | 23 ++++- 4 files changed, 147 insertions(+), 10 deletions(-) create mode 100644 frontend/src/modals/bellcloud/users/CreateUserModal.jsx diff --git a/backend/users/models.py b/backend/users/models.py index aa73365..dffab92 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -4,7 +4,7 @@ from typing import List, Optional # --- Request / Response schemas --- -class UserCreate(BaseModel): +class UserProfile(BaseModel): email: str = "" display_name: str = "" photo_url: str = "" @@ -17,6 +17,10 @@ class UserCreate(BaseModel): quickSettingsPIN: str = "" +class UserCreate(UserProfile): + password: str = "" # request-only — used to create the Firebase Auth account, never persisted to Firestore or echoed back + + class UserUpdate(BaseModel): email: Optional[str] = None display_name: Optional[str] = None @@ -29,7 +33,7 @@ class UserUpdate(BaseModel): quickSettingsPIN: Optional[str] = None -class UserInDB(UserCreate): +class UserInDB(UserProfile): id: str created_time: str = "" lastActive: str = "" diff --git a/backend/users/service.py b/backend/users/service.py index 8175b93..4695ba3 100644 --- a/backend/users/service.py +++ b/backend/users/service.py @@ -84,12 +84,28 @@ def get_user(user_doc_id: str) -> UserInDB: def create_user(data: UserCreate) -> UserInDB: - """Create a new user document in Firestore.""" + """Create a new user: a Firebase Auth account (so they can log into the app + immediately) plus the matching Firestore profile document.""" + if not data.password or len(data.password) < 6: + raise ValidationError("Password must be at least 6 characters.") + if not data.email: + raise ValidationError("Email is required.") + db = get_db() - doc_data = data.model_dump() + doc_data = data.model_dump(exclude={"password"}) doc_data["friendsList"] = [] doc_data["friendsInvited"] = [] + try: + firebase_user = firebase_auth.create_user( + email=data.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.") + doc_data["uid"] = firebase_user.uid + _, doc_ref = db.collection(COLLECTION).add(doc_data) return UserInDB(id=doc_ref.id, **doc_data) @@ -111,13 +127,20 @@ def update_user(user_doc_id: str, data: UserUpdate) -> UserInDB: def delete_user(user_doc_id: str) -> None: - """Delete a user document from Firestore.""" + """Delete a user document from Firestore, plus their Firebase Auth account if one exists.""" db = get_db() doc_ref = db.collection(COLLECTION).document(user_doc_id) doc = doc_ref.get() if not doc.exists: raise NotFoundError("User") + uid = doc.to_dict().get("uid", "") + if uid: + try: + firebase_auth.delete_user(uid) + except firebase_auth.UserNotFoundError: + pass + doc_ref.delete() diff --git a/frontend/src/modals/bellcloud/users/CreateUserModal.jsx b/frontend/src/modals/bellcloud/users/CreateUserModal.jsx new file mode 100644 index 0000000..1d5ba4d --- /dev/null +++ b/frontend/src/modals/bellcloud/users/CreateUserModal.jsx @@ -0,0 +1,97 @@ +// frontend/src/modals/bellcloud/users/CreateUserModal.jsx +// Create a new app user (Firebase Auth account + Firestore profile) from anywhere in the console. + +import { useState, useEffect } from 'react' +import api from '@/lib/api' +import Modal from '@/components/ui/Modal' +import Button from '@/components/ui/Button' +import FormField from '@/components/ui/FormField' + +export default function CreateUserModal({ open, onClose, onCreated }) { + const [email, setEmail] = useState('') + const [displayName, setDisplayName] = useState('') + const [password, setPassword] = useState('') + const [saving, setSaving] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (!open) return + setEmail(''); setDisplayName(''); setPassword(''); setError('') + }, [open]) + + const handleCreate = async () => { + setError('') + if (!email || !displayName || !password) { + setError('Email, display name, and password are required.') + return + } + if (password.length < 6) { + setError('Password must be at least 6 characters.') + return + } + setSaving(true) + try { + const created = await api.post('/users', { + email, + display_name: displayName, + password, + status: 'active', + }) + onCreated(created) + } catch (err) { + setError(err.message || 'Failed to create user.') + } finally { + setSaving(false) + } + } + + return ( + + + + + } + > +
+ setEmail(e.target.value)} + required + placeholder="user@example.com" + autoFocus + /> + setDisplayName(e.target.value)} + required + placeholder="e.g. Jane Smith" + /> + setPassword(e.target.value)} + required + placeholder="Min. 6 characters" + hint="Used to sign into the mobile app immediately." + /> + {error && ( +

{error}

+ )} +
+
+ ) +} diff --git a/frontend/src/pages/bellcloud/users/UserForm.jsx b/frontend/src/pages/bellcloud/users/UserForm.jsx index e9660d8..d8ea9b4 100644 --- a/frontend/src/pages/bellcloud/users/UserForm.jsx +++ b/frontend/src/pages/bellcloud/users/UserForm.jsx @@ -71,7 +71,7 @@ function StatusToggle({ value, onChange }) { function PhotoField({ value, onChange, userId }) { const fileInputRef = useRef(null) - const toast = useToast() + const { toast } = useToast() const [uploading, setUploading] = useState(false) const [hovered, setHovered] = useState(false) @@ -192,7 +192,7 @@ function EyeToggle({ show, onToggle }) { } function PasswordCard({ userId, hasUid }) { - const toast = useToast() + const { toast } = useToast() const [password, setPassword] = useState('') const [confirm, setConfirm] = useState('') const [showPass, setShowPass] = useState(false) @@ -302,9 +302,9 @@ function PasswordCard({ userId, hasUid }) { // ─── Main ──────────────────────────────────────────────────────────────────── export default function UserForm() { - const { id } = useParams() - const navigate = useNavigate() - const toast = useToast() + const { id } = useParams() + const navigate = useNavigate() + const { toast } = useToast() const isEdit = Boolean(id) const [form, setForm] = useState({ @@ -317,6 +317,7 @@ export default function UserForm() { userTitle: '', settingsPIN: '', quickSettingsPIN: '', + password: '', }) const [uid, setUid] = useState('') const [loading, setLoading] = useState(false) @@ -435,6 +436,18 @@ export default function UserForm() { placeholder="+1 234 567 8900" /> + {!isEdit && ( + + )}