firebase - Cloud Firestore: aplicación de nombres de usuario únicos
google-cloud-firestore (4)
El problema
He visto esta pregunta varias veces (también en el contexto de Firebase Real-Time Database), pero no he visto una respuesta convincente. La declaración del problema es bastante simple:
¿Cómo pueden los usuarios (autenticados) elegir un nombre de usuario que aún no se haya tomado?
En primer lugar, el por qué : después de que un usuario se autentica, tiene una identificación de usuario única. Sin embargo, muchas aplicaciones web permiten al usuario elegir un "nombre para mostrar" (cómo desea que el usuario aparezca en el sitio web), para proteger los datos personales de los usuarios (como el nombre real).
La colección de usuarios
Dada una estructura de datos como la siguiente, es posible almacenar un nombre de usuario junto con otros datos para cada usuario:
/users (collection)
/{uid} (document)
- name: "<the username>"
- foo: "<other data>"
Sin embargo, nada impide que otro usuario (con un
{uid}
diferente) almacene el mismo
name
en su registro.
Hasta donde sé, no existe una "regla de seguridad" que nos permita verificar si el
name
ya ha sido escrito por otro usuario.
Nota: es posible una verificación del lado del cliente, pero no es seguro ya que un cliente malintencionado podría omitir la verificación.
El mapeo inverso
Las soluciones populares están creando una colección con un mapeo inverso:
/usernames (collection)
/{name} (document)
- uid: "<the auth {uid} field>"
Dada esta asignación inversa, es posible escribir una regla de seguridad para exigir que un nombre de usuario aún no se haya tomado:
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;
}
y para obligar a un usuario a crear primero un documento de nombres de usuario:
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;
}
Creo que la solución está a medio camino. Sin embargo, todavía hay algunos problemas sin resolver.
Cuestiones / preguntas restantes
Esta implementación ya es bastante complicada, pero ni siquiera resuelve el problema de los usuarios que desean cambiar su nombre de usuario (requiere la eliminación de registros o reglas de actualización, etc.)
Otro problema es que nada impide que un usuario agregue múltiples registros en la colección de
usernames
, arrebatando efectivamente todos los buenos nombres de usuario para sabotear el sistema.
Entonces a las preguntas:
- ¿Existe una solución más simple para imponer nombres de usuario únicos?
-
¿Cómo se puede evitar el spam de la colección de
usernames
? - ¿Cómo se pueden hacer las verificaciones de nombre de usuario sin distinción entre mayúsculas y minúsculas?
Intenté también imponer la existencia de los
users
, con otra regla
exists()
para la colección / usernames y luego cometí una operación de escritura por lotes, sin embargo, esto no parece funcionar (error "
Permisos faltantes o insuficientes
").
Otra nota: he visto soluciones con controles del lado del cliente. PERO ESTOS SON INSEGUROS . Cualquier cliente malintencionado puede modificar el código y omitir verificaciones.
Creé otra solución bastante simple para mí.
Tengo una colección de
usernames
para almacenar valores únicos.
username
está disponible si el documento no existe, por lo que es fácil verificarlo en el front-end.
Además, agregué el patrón
^([a-z0-9_.]){5,30}$
para validar un valor clave.
Verificando todo con las reglas de 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);
}
Las reglas de Firestore no permitirán actualizar el documento del usuario en caso de que el nombre de usuario ya exista o tenga un valor no válido.
Entonces, Cloud Functions se manejará solo en caso de que el nombre de usuario tenga un valor válido y aún no exista. Por lo tanto, su servidor tendrá mucho menos trabajo.
Todo lo que necesita con las funciones en la nube es actualizar la colección 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;
});
Cree una serie de funciones en la nube que se activan cada vez que se agrega, actualiza o elimina un documento en la tabla de
users
.
Las funciones de la nube mantendrán una tabla de búsqueda separada llamada
usernames
, con identificadores de documento establecidos para los nombres de usuario.
Su aplicación front-end puede consultar la colección de nombres de usuario para ver si hay un nombre de usuario disponible.
Aquí está el código TypeScript para las funciones de la nube:
/* 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();
});
Guardo los
usernames
de
usernames
en la misma colección donde cada nombre de usuario ocupa una ID de
document
única.
De esa forma, el nombre de usuario que ya existe no se creará en la base de datos.
@asciimike
en twitter es un desarrollador de reglas de seguridad de Firebase.
Él dice que actualmente no hay forma de imponer la unicidad en una clave en un documento.
https://twitter.com/asciimike/status/937032291511025664
Dado que
firestore
se basa en el
datastore
Google Cloud, hereda este problema.
Ha sido una solicitud de larga data desde 2008.
https://issuetracker.google.com/issues/35875869#c14
Sin embargo, puede lograr su objetivo utilizando
firebase functions
y algunas estrictas
security rules
.
Puede ver toda mi solución propuesta en medio. https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952