Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint Four] - Client Survey
Providing a secure way to update client history
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Structure of this Module PUBLIC
- Lesson 3: [Sprint One] Setting up Firebase PUBLIC
- Lesson 4: [Sprint One] Creating Security Rules with TDD PRO
- Lesson 5: [Sprint One] Testing Authentication PRO
- Lesson 6: [Sprint One] Component Store PRO
- Lesson 7: [Sprint One] Circumventing Firebase Authentication for E2E Tests PRO
- Lesson 8: [Sprint Two] Displaying Client List from Firestore PRO
- Lesson 9: [Sprint Two] - Adding Clients PRO
- Lesson 10: [Sprint Two] - Editing Clients PRO
- Lesson 11: [Sprint Two] - Client Details PRO
- Lesson 12: Preparing for Delivery PRO
- Lesson 13: Configuring for Production PRO
- Lesson 14: [Sprint Three] - Refactoring PRO
- Lesson 15: [Sprint Three] Setting up PWA PRO
- Lesson 16: [Sprint Three] Logout PRO
- Lesson 17: [Sprint Three] Delete a Client PRO
- Lesson 18: [Sprint Three] - Feedback Mechanism PRO
- Lesson 19: [Sprint Three] View Feedback PRO
- Lesson 20: More Styling PRO
- Lesson 21: [Sprint Four] - Refactoring Feedback PRO
- Lesson 22: [Sprint Four] - Feedback Dates PRO
- Lesson 23: [Sprint Four] - Client Survey PRO
- Lesson 24: [Sprint Four] - View Survey PRO
- Lesson 25: Final Touches PRO
- Lesson 26: Conclusion PRO
Lesson Outline
Feat: Send a client a link to the questionnaire
Now it's time to get started on our last major feature for this module, and really a core feature of this entire application. This uses the fundamental novel idea we came up with in the beginning related to the data/security model we designed for our Firestore database.
User story: As a massage therapist, I want to be able to send a client a link to the questionnaire that they can complete online, so that the client doesn't need to spend time filling out the questionnaire once they arrive
Project management
Remember to move the card for this task to the Test
column, and create a new task branch to work on.
We put some thought into how this would all work earlier on in our prototype, so let's review that now. We have our sequence diagram that we created earlier to show the general flow of what should happen:
and we have the security rules we tested in the prototype:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /clients/{client} {
allow update: if true;
allow create, read, delete: if isAdmin();
}
match /{document=**} {
allow read, write: if isAdmin();
}
}
function isAdmin(){
return request.auth != null && request.auth.token.email == '[email protected]' && request.auth.token.email_verified;
}
}
The key problem we had to solve here is that we want an unauthenticated user to be able to update the survey
property of a client in the clients
collection so that they can submit their survey response:
survey: ['JSON STRING 1', 'JSON STRING 2'];
But we don't want people to be able to just update anyone elses survey, and we certainly don't want them to be able to read anybody's survey. Our rules solve half of this problem: only the admin can read the surveys, but anybody can update them.
The way that we have solved this issue will be by supplying the client a link to the page that will contain the survey, and we will include the id
of their document from the clients
collection in the URL. We will then use that id
to determine which document to update. This way the client can only update their survey if they are given a link containing the id
, and they won't be able to update anybody elses because they won't know the id
.
In any case, even if an id
was compromised the worst someone can do is update the form with false data - they won't be able to retrieve any previous results because they don't have read permission. Given that the responses will also be confirmed in person at the appointment, this isn't really an issue.
To complete this user story there will be a few main parts:
- Implement the required security rules
- Implement the survey form
- Create a way for the admin to retrieve the link to the survey form containing the clients
id
(so that they can send the client the link)
We might consider breaking these down into their own smaller issues, each with their own task branch, but I think this work is small enough to just do all at once (I think that now at least).
Updating Security Rules
Let's start by implementing our security rules. We're back to testing Firestore security rules again, hopefully you're starting to get familiar with this!
Let's add our new test to firestore.rules.spec.ts
:
it('unauthenticated users can ONLY update documents in the clients collection', async () => {
const db = getFirestore();
const newDoc = doc(db, 'clients', 'testDoc');
const existingDoc = doc(db, 'clients', 'existingDoc');
await Promise.all([
assertFails(getDoc(existingDoc)), //read
assertFails(setDoc(newDoc, { foo: 'bar' })), // create
assertSucceeds(setDoc(existingDoc, { foo: 'bar' })), // update
assertFails(deleteDoc(existingDoc)), // delete
]);
});
Make sure that it fails by running npm run test:rules
:
FAIL firestore-test/firestore.rules.spec.ts (8.201 s)
Firestore security rules
✓ non admin user can not read/write from the clients collection (3931 ms)
✓ non admin user can not read/write from the notes collection (255 ms)
✓ admin user can read/write from the clients collection (184 ms)
✓ admin user can read/write from the notes collection (115 ms)
✓ unauthenticated users can ONLY create documents in the feedback collection (127 ms)
✕ unauthenticated users can ONLY update documents in the clients collection (120 ms)
● Firestore security rules - unauthenticated users can ONLY update documents in the clients collection
FirebaseError: 7 PERMISSION_DENIED:
false for 'update' @ L11
Great - the update is failing but we want it to succeed. Now let's update the rules in our firestore.rules
file:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /clients/{client} {
allow update: if true;
allow create, read, delete: if isAdmin();
}
match /feedback/{feedbackId}{
allow create: if true;
}
match /{document=**} {
allow read, write: if isAdmin();
}
}
function isAdmin(){
return request.auth != null && request.auth.token.email == '[email protected]' && request.auth.token.email_verified;
}
}
and now let's try the test again:
FAIL firestore-test/firestore.rules.spec.ts
Firestore security rules
✕ non admin user can not read/write from the clients collection (1198 ms)
This is actually causing a previous test to fail, where we checked that a non admin user had no access to the client collection:
it('non admin user can not read/write from the clients collection', async () => {
const db = getFirestore();
const newDoc = doc(db, 'clients', 'testDoc');
const existingDoc = doc(db, 'clients', 'existingDoc');
await Promise.all([
assertFails(getDoc(existingDoc)), //read
assertFails(setDoc(newDoc, { foo: 'bar' })), // create
assertFails(setDoc(existingDoc, { foo: 'bar' })), // update
assertFails(deleteDoc(existingDoc)), // delete
]);
});
We can just delete this test now. Then if we try out tests again:
PASS firestore-test/firestore.rules.spec.ts
Firestore security rules
✓ non admin user can not read/write from the notes collection (1213 ms)
✓ admin user can read/write from the clients collection (161 ms)
✓ admin user can read/write from the notes collection (123 ms)
✓ unauthenticated users can ONLY create documents in the feedback collection (123 ms)
✓ unauthenticated users can ONLY update documents in the clients collection (125 ms)
Perfect! Now let's make sure we deploy those rules using:
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).