Search code examples
firebasefluttergoogle-cloud-firestorefirebase-security

How to create a new Firestore user collection directly from Flutter code


I have been able to get Firebase Authentication to work for Google sign in, Anonymous sign in and from Email and Password sign in, including sending a verification email during email and password sign in thanks to help on stackoverflow.  Everything works as intended.  Now for my next step I am trying to create a user collection in Firestore using the uid created by Firebase Authentication.  I am confident my code is written correctly because I have tested it with (unsecure) Security Rules and the process worked exactly as desired. I have reviewed the Firebase documentation several times but I cannot figure out what is wrong with my Security Rules code. How can I fix my Security rules to allow a new user to create a Screen name that will be added to the user collection in Firestore?  Thanks in advance for the help. 

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid}/jobs/{document=**} {
       allow read, write: if request.auth.uid == uid;
  }

  match /users/{uid}/{document=**} {
       allow read, write: if request.auth.uid == uid;
  }

  }
  
  }


   class HomePage extends StatefulWidget {
      const HomePage({
        Key? key,
        
      }) : super(key: key);
    
      
    
      @override
      State<HomePage> createState() => _HomePageState();
    }
    
    class _HomePageState extends State<HomePage> {
      @override
      void initState() {
        super.initState();
        createUserInFirestore();
      }
    
      Future<void> createUserInFirestore() async {
        final GoogleSignIn googleSignIn = GoogleSignIn();
        final GoogleSignInAccount? user = googleSignIn.currentUser;
        final usersRef = FirebaseFirestore.instance.collection('users');
        final DocumentSnapshot doc = await usersRef.doc(user?.id).get();
        if (!doc.exists) {
          final userName = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => const CreateAccountPage(),
            ),
          );
          usersRef.doc(user?.id).set({
            'id': user?.id,
            'userName': userName,
            'photoUrl': user?.photoUrl,
            'email': user?.email,
            'displayName': user?.displayName,
            'bio': '',
            'timestamp': documentIdFromCurrentDate(),
          });
        doc = await usersRef.doc(user?.id).get();
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return AdaptiveLayoutScaffold(
          drawer: const SideSheet(
            userImage: FakeUserAvatars.stacy,
            userName: 'Stacy James',
          ),
          landscapeBodyWidget: Container(),
          portraitBodyWidget: Container(),
        );
      }
    }

class CreateAccountPage extends StatefulWidget {
  const CreateAccountPage({
    Key? key,
  }) : super(key: key);

  @override
  _CreateAccountPageState createState() => _CreateAccountPageState();
}

class _CreateAccountPageState extends State<CreateAccountPage> {
  final _formKey = GlobalKey<FormState>();
  late String userName;

  void submit() {
    _formKey.currentState?.save();
    Navigator.pop(context, userName);
  }

  @override
  Widget build(BuildContext context) {
    return AdaptiveLayoutScaffold(
      appBar: const Header(
        automaticallyImplyLeading: false,
        pageName: 'User Name',
      ),
      landscapeBodyWidget: Container(),
      portraitBodyWidget: ListView(
        children: [
          Column(
            children: [
              const Padding(
                padding: EdgeInsets.only(top: 16.0),
                child: Center(
                  child: Text('Create a User Name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(16.0),
                child: Form(
                  key: _formKey,
                  child: TextFormField(
                    autovalidateMode: AutovalidateMode.always,
                    decoration: InputDecoration(
                      hintText: 'Must be between 3 and 20 characters',
                      labelText: 'User Name',
                      prefixIcon: Icon(
                        Icons.person,
                        color: Theme.of(context).iconTheme.color,
                      ),
                    ),
                    keyboardType: TextInputType.text,
                    onSaved: (val) => userName = val as String,
                  ),
                ),
              ),
              PlatformElevatedButton(
                onPressed: submit,
                buttonText: 'Create User Name',
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Solution

  • After reading what has been suggested and a few other things I used the Firestore Rules Playground to fix my code and then updated my Auth class to include a new method called createUserInFirestore() to handle the creation of a new user in Firestore using the uid after the user is created by Firebase Authentication.

    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
      
        match /users/{uid}/{document=**} {
           allow read, create, update, delete: if request.auth.uid == uid;  
        }
      } 
    }
    
    abstract class AuthBase {
      User? get currentUser;
    
      Stream<User?> authStateChanges();
      Future<User?> signInWithGoogle();
      Future<User?> createUserWithEmailAndPassword(
        String email,
        String password,
      );
      Future<void> checkEmailVerified(BuildContext context, Timer timer);
      Future<User?> signInWithEmailAndPassword(String email, String password);
      Future<User?> signInAnonymously();
      Future<void> resetPassword(BuildContext context, String email);
      Future<void> confirmSignOut(BuildContext context);
      Future<void> signOut();
    }
    
    class Auth implements AuthBase {
      final _firebaseAuth = FirebaseAuth.instance;
    
      @override
      User? get currentUser => _firebaseAuth.currentUser;
    
      @override
      Stream<User?> authStateChanges() => _firebaseAuth.authStateChanges();
    
      void _createNewUserInFirestore() {
        final User? user = currentUser;
        final CollectionReference<Map<String, dynamic>> usersRef =
            FirebaseFirestore.instance.collection('users');
        usersRef.doc(user?.uid).set({
          'id': user?.uid,
          'screenName': '',
          'displayName': user?.displayName,
          'photoUrl': user?.photoURL,
          'bio': '',
          'darkMode': false,
          'timestamp': documentIdFromCurrentDate(),
        });
      }
    
      @override
      Future<User?> signInWithGoogle() async {
        final GoogleSignIn googleSignIn = GoogleSignIn();
        final GoogleSignInAccount? googleUser = await googleSignIn.signIn();
        if (googleUser != null) {
          final googleAuth = await googleUser.authentication;
          if (googleAuth.idToken != null) {
            final UserCredential userCredential =
                await _firebaseAuth.signInWithCredential(
              GoogleAuthProvider.credential(
                idToken: googleAuth.idToken,
                accessToken: googleAuth.accessToken,
              ),
            );
            _createNewUserInFirestore();
            return userCredential.user;
          } else {
            throw FirebaseAuthException(
              code: FirebaseExceptionString.missingGoogleIDTokenCode,
              message: FirebaseExceptionString.missingGoogleIDTokenMessage,
            );
          }
        } else {
          throw FirebaseAuthException(
            code: FirebaseExceptionString.abortedByUserCode,
            message: FirebaseExceptionString.canceledByUserMessage,
          );
        }
      }
    
      @override
      Future<User?> createUserWithEmailAndPassword(
        String email,
        String password,
      ) async {
        final UserCredential userCredential =
            await _firebaseAuth.createUserWithEmailAndPassword(
          email: email,
          password: password,
        );
        _createNewUserInFirestore();
        return userCredential.user;
      }
    
      @override
      Future<void> checkEmailVerified(BuildContext context, Timer timer) async {
        final User? user = currentUser;
        await user?.reload();
        final User? signedInUser = user;
        if (signedInUser != null && signedInUser.emailVerified) {
          timer.cancel();
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (context) => const HomePage(),
            ),
          );
        }
      }
    
      @override
      Future<User?> signInWithEmailAndPassword(
        String email,
        String password,
      ) async {
        final UserCredential userCredential =
            await _firebaseAuth.signInWithCredential(
          EmailAuthProvider.credential(
            email: email,
            password: password,
          ),
        );
        return userCredential.user;
      }
    
      @override
      Future<void> resetPassword(
        BuildContext context,
        String email,
      ) async {
        try {
          await _firebaseAuth.sendPasswordResetEmail(email: email);
          Navigator.of(context).pop();
        } catch (e) {
          print(
            e.toString(),
          );
        }
      }
    
      @override
      Future<User?> signInAnonymously() async {
        final UserCredential userCredential =
            await _firebaseAuth.signInAnonymously();
        return userCredential.user;
      }
    
      Future<void> _signOut(BuildContext context) async {
        try {
          final AuthBase auth = Provider.of<AuthBase>(
            context,
            listen: false,
          );
          await auth.signOut();
          Navigator.pushAndRemoveUntil<dynamic>(
            context,
            MaterialPageRoute<dynamic>(
              builder: (BuildContext context) => const LandingPage(),
            ),
            (route) => false,
          );
        } catch (e) {
          print(
            e.toString(),
          );
        }
      }
    
      @override
      Future<void> confirmSignOut(BuildContext context) async {
        final bool? didRequestSignOut = await showAlertDialog(
          context,
          cancelActionText: DialogString.cancel,
          content: DialogString.signOutAccount,
          defaultActionText: DialogString.signOut,
          title: DialogString.signOut,
        );
        if (didRequestSignOut == true) {
          _signOut(context);
        }
      }
    
      @override
      Future<void> signOut() async {
        final GoogleSignIn googleSignIn = GoogleSignIn();
        await googleSignIn.signOut();
        await _firebaseAuth.signOut();
      }
    }