firebase - dependency - how to firestore




Cloud Firestore: impondo nomes de usuário exclusivos (3)

Crie uma série de funções da nuvem que são acionadas sempre que um documento é adicionado, atualizado ou excluído na tabela de users . As funções da nuvem manterão uma tabela de pesquisa separada denominada usernames , com IDs de documentos configurados para os nomes de usuários. Seu aplicativo front-end pode, então, consultar a coleção de nomes de usuário para ver se um nome de usuário está disponível.

Aqui está o código TypeScript para as funções da nuvem:

/* Whenever a user document is added, if it contains a username, add that
   to the usernames collection. */
export const userCreated = functions.firestore
  .document('users/{userId}')
  .onCreate((event) => {

    const data = event.data();
    const username = data.username.toLowerCase().trim();

    if (username !== '') {
      const db = admin.firestore();
      /* just create an empty doc. We don't need any data - just the presence 
         or absence of the document is all we need */
      return db.doc(`/usernames/${username}`).set({});
    } else {
      return true;
    }

  });

  /* Whenever a user document is deleted, if it contained a username, delete 
     that from the usernames collection. */
  export const userDeleted = functions.firestore
    .document('users/{userId}')
    .onDelete((event) => {

      const data = event.data();
      const username = data.username.toLowerCase().trim();

      if (username !== '') {
        const db = admin.firestore();
        return db.doc(`/usernames/${username}`).delete();
      }
      return true;
    });

/* Whenever a user document is modified, if the username changed, set and
   delete documents to change it in the usernames collection.  */
export const userUpdated = functions.firestore
  .document('users/{userId}')
  .onUpdate((event, context) => {

    const oldData = event.before.data();
    const newData = event.after.data();

    if ( oldData.username === newData.username ) {
      // if the username didn't change, we don't need to do anything
      return true;
    }

    const oldUsername = oldData.username.toLowerCase().trim();
    const newUsername = newData.username.toLowerCase().trim();

    const db = admin.firestore();
    const batch = db.batch();

    if ( oldUsername !== '' ) {
      const oldRef = db.collection("usernames").doc(oldUsername);
      batch.delete(oldRef);
    }

    if ( newUsername !== '' ) {
      const newRef = db.collection("usernames").doc(newUsername);
      batch.set(newRef,{});
    }

    return batch.commit();
  });

O problema

Já vi essa pergunta várias vezes (também no contexto do Firebase Real-Time Database), mas não vi uma resposta convincente para ela. A declaração do problema é bastante simples:

Como os usuários (autenticados) podem escolher um nome de usuário que ainda não foi usado?

Primeiro de tudo, o porquê : depois que um usuário autentica, ele possui um ID de usuário exclusivo. Muitos aplicativos da web, no entanto, permitem que o usuário escolha um "nome de exibição" (como o usuário deseja aparecer no site), para proteger os dados pessoais dos usuários (como nome real).

A coleção de usuários

Dada uma estrutura de dados como a seguinte, é possível armazenar um nome de usuário junto com outros dados para cada usuário:

/users  (collection)
    /{uid}  (document)
        - name: "<the username>"
        - foo: "<other data>"

No entanto, nada impede que outro usuário (com um {uid} diferente) armazene o mesmo name em seu registro. Tanto quanto eu sei, não há "regra de segurança" que nos permite verificar se o name já foi por outro usuário.

Nota: Uma verificação do lado do cliente é possível, mas insegura, pois um cliente mal-intencionado pode omitir a verificação.

O mapeamento reverso

As soluções populares estão criando uma coleção com um mapeamento reverso:

/usernames  (collection)
    /{name}  (document)
       - uid: "<the auth {uid} field>"

Dado esse mapeamento reverso, é possível escrever uma regra de segurança para impor que um nome de usuário ainda não seja usado:

match /users/{userId} {
  allow read: if true;
  allow create, update: if
      request.auth.uid == userId &&
      request.resource.data.name is string &&
      request.resource.data.name.size() >= 3 &&
      get(/PATH/usernames/$(request.resource.data.name)).data.uid == userId;
}

e forçar um usuário a criar um documento de nome de usuário primeiro:

match /usernames/{name} {
  allow read: if true;
  allow create: if
      request.resource.data.size() == 1 &&
      request.resource.data.uid is string &&
      request.resource.data.uid == request.auth.uid;
}

Eu acredito que a solução está no meio do caminho. No entanto, ainda existem alguns problemas não resolvidos.

Questões / Questões Restantes

Essa implementação já está bastante envolvida, mas nem sequer resolve o problema dos usuários que desejam alterar seu nome de usuário (requer regras de exclusão ou atualização de registros, etc.)

Outro problema é que nada impede que um usuário adicione múltiplos registros na coleção de usernames , roubando efetivamente todos os nomes de usuários bons para sabotar o sistema.

Então, para as perguntas:

  • Existe uma solução mais simples para impor nomes de usuários exclusivos?
  • Como o spam da coleção de usernames pode ser evitado?
  • Como as verificações de nome de usuário podem diferenciar maiúsculas de minúsculas?

Eu tentei também reforçar a existência dos users , com outra regra exists() para a coleção / usernames e, em seguida, cometer uma operação de gravação em lote, no entanto, isso não parece funcionar (erro " Falta ou insuficiente permissões ").

Outra nota: Eu vi soluções com verificações do lado do cliente. MAS ESTES SÃO INSEGUROS . Qualquer cliente mal-intencionado pode modificar o código e omitir verificações.


Criei outra solução simples para mim.

Eu tenho a coleção de usernames para armazenar valores exclusivos. username estará disponível se o documento não existir, por isso é fácil verificar no front-end.

Além disso, adicionei o padrão ^([a-z0-9_.]){5,30}$ para valer um valor de chave.

Verificando tudo com as regras do Firestore:

function isValidUserName(username){
  return username.matches('^([a-z0-9_.]){5,30}$');
}

function isUserNameAvailable(username){
  return isValidUserName(username) && !exists(/databases/$(database)/documents/usernames/$(username));
}

match /users/{userID} {
  allow update: if request.auth.uid == userID 
      && (request.resource.data.username == resource.data.username
        || isUserNameAvailable(request.resource.data.username)
      );
}

match /usernames/{username} {
  allow get: if isValidUserName(username);
}

As regras do Firestore não permitirão a atualização do documento do usuário, caso o nome de usuário já exista ou possua um valor inválido.

Assim, as Cloud Functions só serão tratadas caso o nome de usuário tenha um valor válido e ainda não exista. Então, seu servidor terá muito menos trabalho.

Tudo o que você precisa com as funções da nuvem é atualizar a coleção de usernames :

const functions = require("firebase-functions");
const admin = require("firebase-admin");

admin.initializeApp(functions.config().firebase);

exports.onUserUpdate = functions.firestore
  .document("users/{userID}")
  .onUpdate((change, context) => {
    const { before, after } = change;
    const { userID } = context.params;

    const db = admin.firestore();

    if (before.get("username") !== after.get('username')) {
      const batch = db.batch()

      // delete the old username document from the `usernames` collection
      if (before.get('username')) {
        // new users may not have a username value
        batch.delete(db.collection('usernames')
          .doc(before.get('username')));
      }

      // add a new username document
      batch.set(db.collection('usernames')
        .doc(after.get('username')), { userID });

      return batch.commit();
    }
    return true;
  });

@asciimike no twitter é um desenvolvedor de regras de segurança do @asciimike . Ele diz que atualmente não há como impor exclusividade em uma chave em um documento. https://twitter.com/asciimike/status/937032291511025664

Como o firestore é baseado no datastore Google Cloud, ele herda esse problema. É um pedido antigo desde 2008. https://issuetracker.google.com/issues/35875869#c14

No entanto, você pode alcançar seu objetivo usando firebase functions e algumas security rules rígidas de security rules .

Você pode visualizar toda a minha solução proposta no meio. https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952