I'm trying to make a registration/sign-up process in my Flutter application where the users will need to enter their Voter ID along with other information in order to be registered into the system. I want the Voter ID to be a unique value (like Primary Key in RDBMS) and prevent the user if they try to register using the Voter ID that has already been taken by another registered user. How can I achieve this?
Firebase Cloud Firestore
You have to save all Unique ids while registration and before registration run a query to search that unique id is already registered or not.
FirebaseFirestore.instance
.collection("your_collection_name")
.where("unique_id",
isEqualTo: 'user_input_value')
.get()
.then((value) {
print(value.docs);
if (value.docs.isEmpty) {
// this id is already exist. show some popup
} else {
for (var element in value.docs) {
print(element.id);
// Do your registration here
}
}
});
You'll want to use the voter ID as the key/ID for identifying the user data in the database in that case.
If you're using the Realtime Database, that'd look something like:
Users: {
"voterIdOfUser1": { ... },
"voterIdOfUser2": { ... }
}
If you're using Cloud Firestore, you'd use the same values as the document ID with your Users collection.
I am creating a collection of judges and courthouses. Every judge will be assigned to one courthouse. I have set up my relation to be that courthouse has many judges
I am attempting to do this programmatically when the app loads. I have a function that is able to populate all the fields in judge except the relation to courthouse. My function uses the Strapi API like this
const judge = await strapi.query('judge').create({
name: data[i].name,
},
{
courthouse: data[i].courthouse_name // here is where I think the relation is created
}
)
I am passing in a string that has the name of courthouse, because I don't know the ID of the courthouse in the Courthouse collection.
My question is it possible to create a relation to another collection by anything other than an ID? How can I create a relation to a courthouse by its name?
I couldn't find a way around building a relationship between two models without the ID, so I created a custom solution using the Strapi lifecycle hooks
Essentially what I did I utilized the beforeCreate lifecycle hook to query and find the courthouse that matches the name like this:
// judges.js
async beforeCreate(result, data) {
const courthouse = await strapi.query('courthouse').find(
{courthouse_name:data.courthouse}
); // returns the courthouse that matches the name
result['courthouse'] = courthouse[0].id; // populates the relational field with the
// ID of the courthouse
}
The response object contained the courthouse's ID and I manipulated the data that is being sent to the create command like this:
const judge = await strapi.query('judge').create({
name: data[i].name,
courthouse: data[i].courthouse_name
})
The result is an object that looks like this:
{name: 'Garfield Lucas, courthouse: 7463987}
People register for an event. There are two collections in the database. One for new registrations coming in and one for the registrations of previous years. Both contain an email field as unique identifier.
I would like to know if its possible to check if a newly registered person has registered before in previous years. If so add a field, for example: returningCustomer: true. Otherwise add returningCustomer: false
I am using Mongoose and have a User model for new registrations. I don't have a model (yet) for previously registered users. Would that be neccesary? If it is possible to check if a person has registered before and a field can be added before saving, it might be handy to save the user to the returning customers collection immediatly as well.
I know it is possible to access the current document and collection using a pre save hook, but how about doing a lookup in another collection, write a bit of logic and add a field to the current document pre save?
userSchema.pre('save', function (doc, next) {
const exists = otherCollection.find({ email: doc.email });
exists ? doc.returningCustomer = true : doc.returningCustomer = false;
next();
});
You should have a model for the collection you want to lookup.
Then you can query the other collection before saving the current collection.
CurrentModel.pre('save', async function (next) {
const doc = await OtherModel.find({ field: this.field });
doc.length ? this.returningCustomer = false : this.returningCustomer = true;
next();
});
I´ve a role based data model on Firestore according to googles suggestion here: https://firebase.google.com/docs/firestore/solutions/role-based-access
Security rules are set up correctly and work fine. But now I´ve the problem on how to query for the roles.
This is my data model (one sample document):
id: "1234-5678-91234",
roles:
userId_1:"owner",
userId_2:"editor
title: "This is a sample document"
And this is my Firestore Query in Flutter which gets all documents for a specific user by its ID if the user has assigned the role "owner" for the document:
return firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots().map((snapshot) {
return snapshot.documents.map((catalog) {
return SomeDocumentObject(...);
}).toList();
});
My problem now is, that I need some kind of "OR" clause - which does not exist as far as I know. The query above only retrieves documents for users with role "owner" but I need a query that also retrieves the document if the userId is associated with the role "editor".
I´ve tried "arrayContains:" which also doesn´t seem to work (cause it´s a map).
I´ve read about solutions with two independent queries which doesn´t sound like a good solution due to a lot of overhead.
Maybe someone of you have a hint for me? :)
Thanks & best,
Michael
Firestore doesn't currently have any logical OR operations. You'll have to perform two queries, one for each condition, and merge the results of both queries in the client app.
This is the final solution using RxDart, Observables and .combineLatest() - maybe it helps someone out there:
#override
Stream<List<Catalog>> catalogs(User user) {
// Retrieve all catalogs where user is owner
Observable<QuerySnapshot> ownerCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots());
// Retrieve all catalogs where user is editor
Observable<QuerySnapshot> editorCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "editor")
.snapshots());
// Convert merged stream to list of catalogs
return Observable.combineLatest([ownerCatalogs, editorCatalogs],
(List<QuerySnapshot> snapshotList) {
List<Catalog> catalogs = [];
snapshotList.forEach((snapshot) {
snapshot.documents.forEach((DocumentSnapshot catalog) {
catalogs.add(Catalog(
id: catalog.documentID,
title: catalog.data['title'],
roles: catalog.data['roles'],
));
});
});
return catalogs;
}).asBroadcastStream();
}
The Problem
I have seen this question several times (also in the context of the Firebase Real-Time Database), but I haven't seen a convincing answer to it. The problem statement is fairly simple:
How can (authenticated) users choose a username that hasn't been taken yet?
First of all, the why: After a user authenticates, they have a unique user ID. Many web-apps, however, let the user choose a "display name" (how the user wants to appear on the website), in order to protect the users personal data (like real name).
The Users Collection
Given a data structure like the following it is possible to store a username along with other data for each user:
/users (collection)
/{uid} (document)
- name: "<the username>"
- foo: "<other data>"
However, nothing prevents another user (with a different {uid}) to store the same name in their record. As far as I know, there is no "security rule" that allows us to check if the name has already been by another user.
Note: A client side check is possible, but unsafe as a malicious client could omit the check.
The Reverse Mapping
Popular solutions are creating a collection with a reverse mapping:
/usernames (collection)
/{name} (document)
- uid: "<the auth {uid} field>"
Given this reverse mapping, it is possible to write a security rule to enforce that a username is not already taken:
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;
}
and to force a user to create a usernames document first:
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;
}
I believe the solution is half-way there. However, there are still a few unsolved issues.
Remaining Issues / Questions
This implementation is quite involved already but it doesn't even solve the problem of users that want to change their user name (requires record deletion or update rules, etc.)
Another issue is, nothing prevents a user from adding multiple records in the usernames collection, effectively snatching all good usernames to sabotage the system.
So to the questions:
Is there a simpler solution to enforce unique usernames?
How can spamming the usernames collection be prevented?
How can the username checks be made case-insensitive?
I tried also enforcing existence of the users, with another exists() rule for the /usernames collection and then committing a batch write operation, however, this doesn't seem to work ("Missing or insufficient permissions" error).
Another note: I have seen solutions with client-side checks. BUT THESE ARE UNSAFE. Any malicious client can modify the code, and omit checks.
#asciimike on twitter is a firebase security rules developer.
He says there is currently no way to enforce uniqueness on a key on a document. https://twitter.com/asciimike/status/937032291511025664
Since firestore is based on Google Cloud datastore it inherits this issue. It's been a long standing request since 2008.
https://issuetracker.google.com/issues/35875869#c14
However, you can achieve your goal by using firebase functions and some strict security rules.
You can view my entire proposed solution on medium.
https://medium.com/#jqualls/firebase-firestore-unique-constraints-d0673b7a4952
Created another, pretty simple solution for me.
I have usernames collection to storing unique values. username is available if the document doesn't exist, so it is easy to check on front-end.
Also, I added the pattern ^([a-z0-9_.]){5,30}$ to valide a key value.
Checking everything with Firestore rules:
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);
}
Firestore rules will not allow updating user's document in case if the username already exists or have an invalid value.
So, Cloud Functions will be handling only in case if the username has a valid value and doesn't exist yet. So, your server will have much less work.
Everything you need with cloud functions is to update usernames collection:
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;
});
Create a series of cloud functions that are triggered whenever a document is added, updated, or deleted in the users table. The cloud functions will maintain a separate lookup table named usernames, with document ids set to the usernames. Your front-end app can then query the usernames collection to see if a username is available.
Here is TypeScript code for the cloud functions:
/* 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();
});
This works for me efficiently whereby username must be unique. I am able to add and edit usernames without duplicates.
NOTE: username must be in lowercase always, this eliminates duplicates caused by case sensitivity.
Create users collection:
/users (collection)
/{uid} (document)
- name "the username"
Create usernames collection:
/usernames (collection)
/{name} (document)
- uid "the auth {uid} field"
Then in firestore use the following rules:
match /databases/{database}/documents {
match /usernames/{name} {
allow read,create: if request.auth != null;
allow update: if
request.auth.uid == resource.data.uid;
}
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(/databases/$(database)/documents/usernames/$(request.resource.data.name)).data.uid == userId;
}
}
I store the usernames in the same collection where each username occupies a unique document ID. That way the username which already exists will not be created in the database.
One possible solution is to store all usernames in a single document's usernames field and then permit only additions to that document using sets in Rules:
match /users/allUsernames {
function validateNewUsername() {
// Variables in functions are allowed.
let existingUsernames = resource.data.usernames;
let newUsernames = request.resource.data.usernames;
let usernameToAdd = newUsernames[newUsernames.size() - 1];
// Sets are a thing too.
let noRemovals = existingUsernames.toSet().difference(newUsernames.toSet()).size() == 0;
let usernameDoesntExistYet = !(usernameToAdd in existingUsernames.toSet());
let exactlyOneAddition = newUsernames.size() == existingUsernames.size() + 1;
return noRemovals && usernameDoesntExistYet && exactlyOneAddition;
}
allow update: if request.resource.data.keys().hasOnly(['usernames']) && validateNewUsername();
}
If you wanted to make a mapping from username -> uid (for validating other parts of the ruleset) this is possible in a single document too. You can just take the keyset of the document and do the same set operations as above.
This answer addresses your second concern about adding multiple records in the usernames collection. I'm not sure if this is the best method, but I believe a possible approach to prevent a given user from creating multiple username documents is writing an onCreate cloud function which checks if the user has an existing username document when a new username document is created. If the user does, then the cloud function can delete this document to prevent any malicious username parking.
Store the max integer user id used in the database in another collection. Query that collection everytime to find the max user id. You can even store other max ids in this collection. It can look something like this:
MaxIDCollection:
maxStudentIDDocument={ maxID: 55 } //lets say the max user id in db is 55
maxCourseIDDocument={ maxID: 77 }
Make sure to update the maxIDs everytime you add a new Student or Course.
If in future you add a new Student then by querying this collection you can know "if 55 is max then the new Student should get 56 as id."