Flutter Firebase - Allow null `request.auth.uid` to read collection documents

Viewed 190

A social media-type application that I'm developing allows the user to select a unique username upon signup, the only issue is that the app handles username novelty verification client side. That is to say that the app retrieves all usernames from the relevant collection, and checks whether or not the requested username already exists within that list.

Unfortunately, with the way my firebase security rules are set up (please see below), the user must first exist before this aforementioned 'username novelty verification' can take place.

This means that no matter if the username is unique or not, the user is given an account under their desired email address first. This results in accounts being setup that have failed this verification, and have a subsequently null username field.

I understand that handling username verification client-side is inefficient at best, and dirty at worst, so if you could suggest any 'Firebase-side' verification methods, perhaps using Firebase 'Functions', that would be awesome.

Thanks


Firebase security rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userID} {
      allow read, create: if request.auth.uid != null;
      allow update, write, delete: if (
        (
            request.resource.data.email == resource.data.email &&
             request.resource.data.id == resource.data.id && 
               request.auth.uid == userID
        ) && 
            (request.resource.data.userBio != resource.data.userBio ||
              request.resource.data.username != resource.data.username || 
                request.resource.data.location != resource.data.location ||
                 request.resource.data.favPlaces != resource.data.favPlaces ||
                   request.resource.data.following != resource.data.following ||
                     request.resource.data.likedTrips != resource.data.likedTrips)
      );
    }
...

Email signup routine:

Future signUpWithEmail({
    @required String email,
    @required String password,
    @required String username,
    @required String userBio,
    @required List favPlaces,

    @required BuildContext context,

  }) async {

    var authResult;

    try {
      authResult = await _firebaseAuth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );

      sendUserEmailVerification(authResult.user);

    } catch (e) {
      print(e.message);
    }

    final CollectionReference collectionReference = FirebaseFirestore.instance.collection('users');
    final documents = await collectionReference.where("username").get();

    List results = [];

    for (int i = 0; i < documents.docs.length; i++) {
      results.add(documents.docs[i]["username"]);
    }

    if (!results.contains(username)) {
      if (favPlaces.toString() != "[]") {
        try {

          await _firestoreService.createUser(
            TRUser(
              id: authResult.user.uid,
              email: email,
              username: username,
              userBio: userBio,
              following: [],
              favPlaces: favPlaces,
              likedTrips: []
            )
          );

          Navigator.push(context, CupertinoPageRoute(builder: (context) => WelcomePage(username:username)));
          
          return authResult.user != null;

        } catch (e) {
          return e;

        }
      } else {

        showDialog(
          context: context,
          builder: (BuildContext context) => CupertinoAlertDialog(
            title: Text("please select at least 1 location"),
            actions: <Widget>[
              CupertinoDialogAction(
                onPressed: () => Navigator.pop(context),
                isDefaultAction: true,
                child: Text('okay'),
              )
            ],
          )
        );

      }
    } else {
      
      showDialog(
        context: context,
        builder: (BuildContext context) => CupertinoAlertDialog(
          title: Text("this username is already in use"),
          actions: <Widget>[
            CupertinoDialogAction(
              onPressed: () => Navigator.pop(context),
              isDefaultAction: true,
              child: Text('okay'),
            )
          ],
        )
      );

    }
  }
1 Answers

If your users are authenticating first, then taken to create a unique username, then there's a solution for your problem.

Create a new collection that has a document that contains only usernames. You use one document to save up on costs from reads, because every new user will request a get() for this doc.

On the top of your rules, you create a rule for this connection, where you allow only authenticated users to read it.

After it's download, you can do your check to validate if the name is unique or not, if the user finishes up, you add this newly created username to this document,.so other new users stay updated.

Related