Skip to content

Commit 88e1c40

Browse files
authored
Firestore server-side deletes solution (#17)
1 parent 31a85f4 commit 88e1c40

File tree

8 files changed

+341
-1
lines changed

8 files changed

+341
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ keyfile.json
33
service-account.json
44

55
.firebaserc
6-
firebase.json
6+
firebase-debug.log
77

88
package-lock.json
99
lerna-debug.log

firestore/solution-deletes/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Solution: Recursive Deletes
2+
3+
This solution shows how to write a Cloud Function that deletes data
4+
in Cloud Firestore and securely call this function from your
5+
mobile app or website.
6+
7+
## Setup
8+
9+
### On your development machine
10+
11+
1. Set up your Firebase project in the `solution-deletes` directory
12+
by running `firebase init`. This solution uses both Firebase Hosting
13+
and Cloud Functions for Firebase.
14+
1. Generate a Firebase token using the `firebase login:ci` command.
15+
1. Add the token to your Cloud Functions runtime configuration using the
16+
following command:
17+
```
18+
firebase functions:config:set fb.token="YOUR_TOKEN_HERE"
19+
```
20+
1. Run `firebase deploy --only functions` to deploy the Cloud Functions.
21+
1. Run `firebase serve --only hosting` to run a local version of the
22+
application.
23+
24+
### In your browser
25+
26+
1. Enable the **Identity and Access Management (IAM)** API on your project
27+
in the Google Cloud console by visiting:
28+
https://console.developers.google.com/apis/api/iam.googleapis.com/overview
29+
1. In the [IAM page][iam-page] of the Google Cloud console, find the service account
30+
called the "App Engine default service account" and grant it the
31+
"Service Account Token Creator" role.
32+
33+
## Usage
34+
35+
1. Visit `http://localhost:5000` to see the running sample.
36+
1. Click the **SIGN IN** button. This will call the Cloud Function
37+
you deployed to generate a custom sign in token, and then
38+
use that token to sign in on the client.
39+
1. Enter the path of the document or collection you would like
40+
to delete, for example `things/thing1`, then click **DELETE**.
41+
42+
[iam-page]: https://console.cloud.google.com/project/_/iam-admin
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"hosting": {
3+
"public": "public",
4+
"ignore": [
5+
"firebase.json",
6+
"**/.*",
7+
"**/node_modules/**"
8+
]
9+
}
10+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const admin = require('firebase-admin');
2+
const firebase_tools = require('firebase-tools');
3+
const functions = require('firebase-functions');
4+
5+
admin.initializeApp();
6+
7+
/**
8+
* Callable function that creates a custom auth token with the
9+
* custom attribute "admin" set to true.
10+
*
11+
* See https://firebase.google.com/docs/auth/admin/create-custom-tokens
12+
* for more information on creating custom tokens.
13+
*
14+
* @param {string} data.uid the user UID to set on the token.
15+
*/
16+
exports.mintAdminToken = functions.https.onCall((data, context) => {
17+
const uid = data.uid;
18+
19+
return admin
20+
.auth()
21+
.createCustomToken(uid, { admin: true })
22+
.then(function(token) {
23+
return { token: token };
24+
});
25+
});
26+
27+
// [START recursive_delete_function]
28+
/**
29+
* Initiate a recursive delete of documents at a given path.
30+
*
31+
* The calling user must be authenticated and habe the custom "admin" attribute
32+
* set to true on the auth token.
33+
*
34+
* This delete is NOT an atomic operation and it's possible
35+
* that it may fail after only deleting some documents.
36+
*
37+
* @param {string} data.path the document or collection path to delete.
38+
*/
39+
exports.recursiveDelete = functions
40+
.runWith({
41+
timeoutSeconds: 540,
42+
memory: '2GB'
43+
})
44+
.https.onCall((data, context) => {
45+
// Only allow admin users to execute this function.
46+
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
47+
throw new functions.https.HttpsError(
48+
'permission-denied',
49+
'Must be an administrative user to initiate delete.'
50+
);
51+
}
52+
53+
const path = data.path;
54+
console.log(
55+
`User ${context.auth.uid} has requested to delete path ${path}`
56+
);
57+
58+
// Run a recursive delete on the given document or collection path.
59+
// The 'token' must be set in the functions config, and can be generated
60+
// at the command line by running 'firebase login:ci'.
61+
return firebase_tools.firestore
62+
.delete(path, {
63+
project: process.env.GCLOUD_PROJECT,
64+
recursive: true,
65+
yes: true,
66+
token: functions.config().fb.token
67+
})
68+
.then(() => {
69+
return {
70+
path: path
71+
};
72+
});
73+
});
74+
// [END recursive_delete_function]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "solution-deletes",
3+
"version": "1.0.0",
4+
"description": "Firestore delete data solution",
5+
"main": "index.js",
6+
"engines": {
7+
"node": "8"
8+
},
9+
"author": "",
10+
"license": "Apache-2.0",
11+
"dependencies": {
12+
"firebase-admin": "^6.0.0",
13+
"firebase-functions": "^2.0.5",
14+
"firebase-tools": "^4.2.1"
15+
}
16+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
body {
2+
background: #ECEFF1;
3+
color: rgba(0,0,0,0.87);
4+
font-family: Roboto, Helvetica, Arial, sans-serif;
5+
margin: 0;
6+
padding: 0;
7+
}
8+
9+
hr {
10+
margin-top: 16px;
11+
margin-bottom: 16px;
12+
}
13+
14+
#form-signin {
15+
display: grid;
16+
}
17+
18+
#form-delete {
19+
display: grid;
20+
grid-template-rows: 1fr 1fr;
21+
}
22+
23+
#message {
24+
background: white;
25+
max-width: 480px;
26+
margin: 100px auto 16px;
27+
padding: 32px 24px;
28+
border-radius: 3px;
29+
}
30+
31+
#message h2 {
32+
color: #ffa100;
33+
font-weight: bold;
34+
font-size: 16px;
35+
margin: 0 0 8px;
36+
}
37+
38+
#message h1 {
39+
font-size: 22px;
40+
font-weight: 300;
41+
color: rgba(0,0,0,0.6);
42+
margin: 0 0 16px;
43+
}
44+
45+
.btn {
46+
border: none;
47+
margin-top: 4px;
48+
margin-bottom: 4px;
49+
50+
display: inline-block;
51+
text-align: center;
52+
background: #039be5;
53+
text-transform: uppercase;
54+
text-decoration: none;
55+
color: white;
56+
padding: 8px;
57+
border-radius: 4px;
58+
}
59+
60+
.btn:disabled {
61+
background: #bdbdbd;
62+
box-shadow: none;
63+
}
64+
65+
.shadow {
66+
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
67+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1">
6+
<title>Firestore - Delete Demo</title>
7+
8+
<script defer src="/__/firebase/5.5.1/firebase-app.js"></script>
9+
<script defer src="/__/firebase/5.5.1/firebase-auth.js"></script>
10+
<script defer src="/__/firebase/5.5.1/firebase-functions.js"></script>
11+
<script defer src="/__/firebase/init.js"></script>
12+
13+
<link rel="stylesheet" href="index.css" />
14+
<script defer src="index.js"></script>
15+
</head>
16+
<body>
17+
<div id="message" class="shadow">
18+
<h2>Cloud Firestore Demo</h2>
19+
<h1>Server-Side Deletes</h1>
20+
<p>Use the form below to initiate a delete.</p>
21+
22+
<div id="form-signin">
23+
<input type="submit" class="btn shadow" id="btn-signin" value="Sign In">
24+
</div>
25+
26+
<hr />
27+
28+
<div id="form-delete">
29+
<input type="text" id="input-delete" placeholder="Path to delete...">
30+
<input type="submit" class="btn shadow" id="btn-delete" value="Delete">
31+
</div>
32+
33+
34+
<p id="status">
35+
Logs:
36+
<ul id="log-list">
37+
</ul>
38+
</p>
39+
</div>
40+
</body>
41+
</html>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
var firebase = firebase || {};
2+
3+
// [START call_delete_function]
4+
/**
5+
* Call the 'recursiveDelete' callable function with a path to initiate
6+
* a server-side delete.
7+
*/
8+
function deleteAtPath(path) {
9+
var deleteFn = firebase.functions().httpsCallable('recursiveDelete');
10+
deleteFn({ path: path })
11+
.then(function(result) {
12+
logMessage('Delete success: ' + JSON.stringify(result));
13+
})
14+
.catch(function(err) {
15+
logMessage('Delete failed, see console,');
16+
console.warn(err);
17+
});
18+
}
19+
// [END call_delete_function]
20+
21+
/**
22+
* Call the 'mintAdminToken' callable function to get a custom token that
23+
* makes us an admin user, then sign in.
24+
*/
25+
function signInAsAdmin() {
26+
var tokenFn = firebase.functions().httpsCallable('mintAdminToken');
27+
tokenFn({ uid: 'user1234' }).then(function (res) {
28+
return firebase.auth().signInWithCustomToken(res.data.token);
29+
});
30+
}
31+
32+
/**
33+
* Helper function: set the signed-in state of the UI.
34+
*/
35+
function setSignedIn(signedIn) {
36+
if (signedIn) {
37+
logMessage('Signed in.');
38+
} else {
39+
logMessage('Not signed in.');
40+
}
41+
42+
setEnabled('input-delete', signedIn);
43+
setEnabled('btn-delete', signedIn);
44+
setEnabled('btn-signin', !signedIn);
45+
}
46+
47+
/**
48+
* Helper function: log a message to the UI.
49+
*/
50+
function logMessage(msg) {
51+
var msgLi = document.createElement('li');
52+
msgLi.innerText = msg;
53+
document.getElementById('log-list').appendChild(msgLi);
54+
}
55+
56+
/**
57+
* Helper function: enable or disable a form element.
58+
*/
59+
function setEnabled(id, enabled) {
60+
if (enabled) {
61+
document.getElementById(id).removeAttribute('disabled');
62+
} else {
63+
document.getElementById(id).setAttribute('disabled', true);
64+
}
65+
}
66+
67+
/**
68+
* Set up the UI:
69+
* - Click listeners.
70+
* - Auth state listener.
71+
*/
72+
document.addEventListener('DOMContentLoaded', function() {
73+
firebase.auth().onAuthStateChanged(function(user) {
74+
if (user) {
75+
setSignedIn(true);
76+
} else {
77+
setSignedIn(false);
78+
}
79+
});
80+
81+
document.getElementById('btn-delete').addEventListener('click', function() {
82+
var deleteInput = document.getElementById('input-delete');
83+
var deletePath = deleteInput.value;
84+
deleteAtPath(deletePath);
85+
});
86+
87+
document.getElementById('btn-signin').addEventListener('click', function() {
88+
signInAsAdmin();
89+
});
90+
});

0 commit comments

Comments
 (0)