Skip to content

Instantly share code, notes, and snippets.

@pujitm
Last active January 13, 2024 19:07
Show Gist options
  • Save pujitm/c9e03333011d8ccbc12d2506fa96feb8 to your computer and use it in GitHub Desktop.
Save pujitm/c9e03333011d8ccbc12d2506fa96feb8 to your computer and use it in GitHub Desktop.
TypeScript Cloud Functions for User Privacy on Firebase
/**
* Message from Editor:
* This is the TypeScript translation of `functions/index.js` in the User Privacy Firebase repo.
* [https://github.com/firebase/user-privacy]
* The License has not been altered.
*/
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
/**
Paths for clearing and exporting data.
All instances of `UID_VARIABLE` in the JSON are replaced by the user's uid at runtime.
See function `replaceUID` for details
*/
const userPrivacyPaths = require('./user_privacy.json')
admin.initializeApp(functions.config().firebase)
const db = admin.database();
const firestore = admin.firestore();
const storage = admin.storage();
const FieldValue = admin.firestore.FieldValue;
/**
* App-specific default bucket for storage. Used to upload exported json and in
* sample json of clearData and exportData paths.
*/
const exportDataBucket = userPrivacyPaths.exportDataUploadBucket;
/**
* The clearData function removes personal data from the RealTime Database,
* Storage, and Firestore. It waits for all deletions to complete, and then
* returns a success message.
*
* Triggered by a user deleting their account.
*/
export const clearData = functions.auth.user().onDelete((user) => {
const uid = user.uid
const databasePromise = clearDatabaseData(uid)
const storagePromise = clearStorageData(uid)
const firestorePromise = clearFirestoreData(uid)
return Promise.all([databasePromise, firestorePromise, storagePromise])
.then(() => console.log(`Successfully removed data for user #${uid}.`)
);
})
/**
* Delete data from all specified paths from the Realtime Database. To add or
* remove a path, edit the `database[clearData]` array in `user_privacy.json`.
*
* This function is called by the top-level `clearData` function.
* Returns a list of Promises
* @param uid
*/
const clearDatabaseData = (uid) => {
const paths = userPrivacyPaths.database.clearData
const promises = []
for (let i = 0; i < paths.length; i++) {
const path = replaceUID(paths[i], uid);
promises.push(db.ref(path).remove().catch((error) => {
// Avoid execution interuption.
console.error('Error deleting data at path: ', path, error);
}));
}
return Promise.all(promises).then(() => uid);
};
/**
Clear all specified files from the Realtime Database. To add or remove a
path, edit the `storage[clearData]` array in `user_privacy.json`.
This function is called by the top-level `clearData` function.
Returns a list of Promises
*/
const clearStorageData = (uid) => {
const paths = userPrivacyPaths.storage.clearData;
const promises = [];
for (let i = 0; i < paths.length; i++) {
const bucketName = replaceUID(paths[i][0], uid)
const path = replaceUID(paths[i][1], uid)
const bucket = storage.bucket(bucketName)
const file = bucket.file(path)
promises.push(file.delete().catch((error) => {
console.error('Error deleting file: ', path, error);
}));
}
return Promise.all(promises).then(() => uid);
}
/**
Clear all specified paths from the Firestore Database. To add or remove a
path, edit the `firestore[clearData]` array in `user_privacy.json`.
This function is called by the top-level `clearData` function.
Returns a list of Promises
*/
const clearFirestoreData = (uid) => {
const paths = userPrivacyPaths.firestore.clearData
const promises = []
for (let i = 0; i < paths.length; i++) {
const entry = paths[i]
const entryCollection = replaceUID(entry.collection, uid)
const entryDoc = replaceUID(entry.doc, uid)
const docToDelete = firestore.collection(entryCollection).doc(entryDoc)
if ('field' in entry) {
const entryField = replaceUID(entry.field, uid)
const update = {}
update[entryField] = FieldValue.delete()
promises.push(docToDelete.update(update).catch((err) => {
console.error('Error deleting field: ', err)
}));
} else if (docToDelete) {
promises.push(docToDelete.delete().catch((err) => {
console.error('Error deleting document: ', err)
}));
}
}
return Promise.all(promises).then(() => uid)
}
/**
The `exportData` function reads and copys data from the RealTime Database,
Storage, and Firestore. It waits to complete reads for all three, and then
uploads a JSON file of the exported data to storage and returns a success
message.
Because the resulting file will contain personal information, it's important
to use Firebase Security Rules to make these files readable only by the user.
See the `storage.rules` file for an example.
Triggered by an http request.
*/
export const exportData = functions.https.onRequest(async (req, response) => {
const body = JSON.parse(req.body)
const uid = body.uid
try {
let databaseData = await exportDatabaseData(uid)
let firestoreData = await exportFirestoreData(uid)
let storageData = await exportStorageData(uid)
const exportData = {
database: databaseData,
firestore: firestoreData,
storage: storageData
}
console.log(`Success! Completed export for user ${uid}.`)
uploadToStorage(uid, exportData) // Change to any server you need/want
response.json({ exportComplete: true })
} catch (error) {
// Handle the error
console.log(error)
response.status(500).send(error)
}
});
/**
Read and copy the specified paths from the RealTime Database. To add or
remove a path, edit the `database[exportData]` array in `user_privacy.json`.
This function is called by the top-level `exportData` function.
Returns a Promise.
*/
const exportDatabaseData = (uid) => {
const paths = userPrivacyPaths.database.exportData
const promises = []
const exportData = {}
for (let i = 0; i < paths.length; i++) {
const path = replaceUID(paths[i], uid)
promises.push(db.ref(path).once('value').then((snapshot) => {
const read = snapshot.val()
if (read !== null) {
exportData[snapshot.key] = read
}
}).catch((err) => {
console.error('Error encountered while exporting Database data: ', err)
}));
}
return Promise.all(promises).then(() => exportData)
}
/**
Read and copy the specified paths from the Firestore Database. To add or
remove a path, edit the `firestore[exportData]` array in `user_privacy.json`.
This function is called by the top-level `exportData` function.
Returns a Promise.
*/
const exportFirestoreData = (uid) => {
const paths = userPrivacyPaths.firestore.exportData
const promises = []
const exportData = {}
for (let i = 0; i < paths.length; i++) {
let entry = paths[i]
let entryCollection = entry.collection
let entryDoc = replaceUID(entry.doc, uid)
let exportRef = firestore.collection(entryCollection).doc(entryDoc)
let path = `${entryCollection}/${entryDoc}`
promises.push(exportRef.get().then((doc) => {
if (doc.exists) {
let read = doc.data()
if ('field' in entry) {
let entryField = replaceUID(entry.field, uid)
path = `${path}/${entryField}`
read = read[entryField]
}
exportData[path] = read
}
}).catch((err) => {
console.error('Error encountered while exporting from firestore: ', err)
}));
};
return Promise.all(promises).then(() => exportData)
};
/**
In the case of Storage, a read-only copy of each file is created, accessible
only to the user, and a list of copied files is added to the final JSON. It's
essential that the Firebase Security Rules for Storage restrict access of the
copied files to the given user.
This implementation works is designed for accounts on the free tier. To use
multiple buckets, specify a desination bucket arg to the `copy` method.
To add or remove a path, edit the `database[exportData]` array in
`user_privacy.json`.
This function is called by the top-level `exportData` function.
Returns a Promise.
*/
const exportStorageData = (uid) => {
const paths = userPrivacyPaths.storage.exportData
const promises = []
const exportData = {}
for (let i = 0; i < paths.length; i++) {
const entry = paths[i]
const entryBucket = replaceUID(entry[0], uid)
const path = replaceUID(entry[1], uid)
const sourceBucket = storage.bucket(entryBucket)
const sourceFile = sourceBucket.file(path)
const destinationPath = `exportData/${uid}/${path}`
let copyPromise = sourceFile.copy(destinationPath).catch((err) =>
console.log('There is an error copying the promise, but keep going.')
)
// Make copyPromise succeed even if it fails:
copyPromise = copyPromise
// Add the copy task to the array of Promises
promises.push(copyPromise)
exportData[`${entryBucket}/${path}`] = `${exportDataBucket}/${destinationPath}`
}
return Promise.all(promises).then(() => exportData)
}
/**
Upload json to Storage, under a filename of the user's uid. This is the final
result of the exportData function; because this file will contain personal
information, it's important to use Firebase Security Rules to make these
files readable only by the user with the same uid.
Called by the top-level exportData function.
*/
const uploadToStorage = (uid, exportedData) => {
const json = JSON.stringify(exportedData)
const bucket = storage.bucket(exportDataBucket)
const file = bucket.file(`exportData/${uid}/export.json`)
return file.save(json)
}
/**
* Replaces the string `UID_VARIABLE` inside parameter `str` with the parameter `uid`.
* @param str
* @param uid
*/
const replaceUID = (str, uid) => {
return str.replace(/UID_VARIABLE/g, uid);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment