Le problème
J'ai vu cette question plusieurs fois (également dans le contexte de la base de données en temps réel Firebase), mais je n'y ai pas trouvé de réponse convaincante. L'énoncé du problème est assez simple:
Comment les utilisateurs (authentifiés) peuvent-ils choisir un nom d'utilisateur qui n'a pas encore été utilisé?
Tout d'abord, le why: après l'authentification d'un utilisateur, il dispose d'un ID utilisateur unique. Cependant, de nombreuses applications Web permettent à l'utilisateur de choisir un "nom d'affichage" (comment l'utilisateur souhaite apparaître sur le site Web), afin de protéger les données personnelles de l'utilisateur (comme un nom réel).
La collection des utilisateurs
Avec une structure de données comme celle-ci, il est possible de stocker un nom d'utilisateur avec d'autres données pour chaque utilisateur:
/users (collection)
/{uid} (document)
- name: "<the username>"
- foo: "<other data>"
Cependant, rien n'empêche un autre utilisateur (avec un {uid}
différent) de stocker la même name
dans leur enregistrement. Autant que je sache, aucune "règle de sécurité" ne nous permet de vérifier si la name
a déjà été faite par un autre utilisateur.
Remarque: une vérification côté client est possible, mais non sécurisée, car un client malveillant pourrait l'omettre.
La cartographie inverse
Les solutions populaires créent une collection avec une cartographie inverse:
/usernames (collection)
/{name} (document)
- uid: "<the auth {uid} field>"
Avec ce mappage inverse, il est possible d'écrire une règle de sécurité pour imposer qu'un nom d'utilisateur n'est pas déjà pris:
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;
}
et pour forcer un utilisateur à créer d'abord un document de noms d'utilisateurs:
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;
}
Je crois que la solution est à mi-chemin. Cependant, il reste encore quelques problèmes non résolus.
Autres questions/Questions
Cette implémentation est déjà compliquée mais elle ne résout même pas le problème des utilisateurs qui veulent changer leur nom d'utilisateur (nécessite la suppression d'un enregistrement ou des règles de mise à jour, etc.)
Un autre problème est que rien n'empêche un utilisateur d'ajouter plusieurs enregistrements dans la collection usernames
, en arrachant efficacement tous les bons noms d'utilisateurs pour saboter le système.
Alors aux questions:
usernames
?J'ai également essayé d'appliquer la règle users
, avec une autre règle exists()
pour la collection/usernames, puis de valider une opération d'écriture par lots. Cependant, cela ne semble pas fonctionner (erreur "manquant ou insuffisante").
Autre remarque: j'ai vu des solutions avec des contrôles côté client. MAIS CE SONT DANGEREUX. Tout client malveillant peut modifier le code et omettre les vérifications.
@asciimike
sur Twitter est un développeur de règles de sécurité firebase ..__ Il ajoute qu’il n’existe actuellement aucun moyen d’appliquer l’unicité sur une clé d’un document. https://Twitter.com/asciimike/status/93703232291511025664
firestore
étant basé sur Google Cloud datastore
, il hérite de ce problème. Cette demande est formulée de longue date depuis 2008 . https://issuetracker.google.com/issues/35875869#c14
Cependant, vous pouvez atteindre votre objectif en utilisant firebase functions
et quelques security rules
stricts.
Vous pouvez voir toute la solution proposée sur support . https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952
Crée une autre solution assez simple pour moi.
J'ai une collection usernames
pour stocker des valeurs uniques. username
est disponible si le document n'existe pas, il est donc facile de le vérifier en frontal.
De plus, j'ai ajouté le motif ^([a-z0-9_.]){5,30}$
pour valider une valeur de clé.
Tout vérifier avec les règles 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);
}
Les règles Firestore n'autoriseront pas la mise à jour du document de l'utilisateur si le nom d'utilisateur existe déjà ou si sa valeur est invalide.
Ainsi, les fonctions de cloud ne seront traitées que dans le cas où le nom d'utilisateur a une valeur valide et n'existe pas encore. Ainsi, votre serveur aura beaucoup moins de travail.
Tout ce dont vous avez besoin avec les fonctions cloud consiste à mettre à jour la collection 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;
});
Créez une série de fonctions de cloud déclenchées chaque fois qu'un document est ajouté, mis à jour ou supprimé dans la table users
. Les fonctions de cloud conservent une table de recherche distincte nommée usernames
, avec les identifiants de document définis pour les noms d'utilisateur. Votre application frontale peut ensuite interroger la collection de noms d'utilisateurs pour savoir si un nom d'utilisateur est disponible.
Voici le code TypeScript pour les fonctions cloud:
/* 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();
});
Je stocke la usernames
dans la même collection où chaque nom d'utilisateur occupe un ID document
unique. Ainsi, le nom d'utilisateur qui existe déjà ne sera pas créé dans la base de données.