Skip to content

Instantly share code, notes, and snippets.

@samthecodingman
Created December 17, 2019 16:17
Show Gist options
  • Save samthecodingman/23fb8be9aa0e66d92381b09cbb2d45cd to your computer and use it in GitHub Desktop.
Save samthecodingman/23fb8be9aa0e66d92381b09cbb2d45cd to your computer and use it in GitHub Desktop.
Defines two Cloud Functions for Firebase to bundle data from the Realtime Database for export to Cloud Storage for Firebase.
/**
* Copyright 2019 Samuel Jones (@samthecodingman). All Rights Reserved.
* Copyright 2016 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.
*
*/
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const {JWT} = require('google-auth-library'); // installed by Storage APIs
admin.initializeApp(); // config from ENV vars
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const cookieParser = require('cookie-parser');
// TODO: Explore using same credentials as Admin SDK
const serviceAccount = require('path/to/serviceAccountKey.json');
const restDatabaseScopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/firebase.database'
];
var jwtClient = new JWT(
serviceAccount.client_email,
null,
serviceAccount.private_key,
restDatabaseScopes
);
// All bundler tasks will be stored here
// "rules": {
// "cloudBundlerTasks": {
// ".write": false,
// ".read": "data.child('data/requestedBy').val() === auth.uid"
// }
// }
const BUNDLER_TASKS_DATABASE_PARENT = '/cloudBundlerTasks'
const whitelist = ['https://your-example-site.com'];
const corsOptions = {
origin: function (origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'))
}
}
}
// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = async (req, res, next) => {
console.log('Check if request is authorized with Firebase ID token');
if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
!(req.cookies && req.cookies.__session)) {
console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
'Make sure you authorize your request by providing the following HTTP header:',
'Authorization: Bearer <Firebase ID Token>',
'or by passing a "__session" cookie.');
res.status(403).send('Unauthorized');
return;
}
let idToken;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split('Bearer ')[1];
} else if(req.cookies) {
console.log('Found "__session" cookie');
// Read the ID Token from cookie.
idToken = req.cookies.__session;
} else {
// No cookie
res.status(403).send('Unauthorized');
return;
}
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
console.log('ID Token correctly decoded', decodedIdToken);
req.user = decodedIdToken;
next();
return;
} catch (error) {
console.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
return;
}
};
const bundlerApp = express();
bundlerApp.use(cors(corsOptions));
bundlerApp.use(cookieParser);
bundlerApp.use(bodyParser.json());
bundlerApp.use(validateFirebaseIdToken);
/*
* HTTP Request function used to create a download worker job
*
* Returns a JSON object with the created Job ID and a database location to listen to for updates.
*/
bundlerApp.post('/submit', (req, res) => {
let pushRef = admin.database().ref(BUNDLER_TASKS_DATABASE_PARENT).push();
if (!req.body.path) {
res.status(400).send('Missing path in body');
return;
}
pushRef.set({
status: 'new',
data: {
path: req.body.path,
requestedBy: req.user.uid,
createdAt: firebase.database.ServerValue.TIMESTAMP
}
})
.then(() => {
res.status(202).json({ // 202 = HTTP_CREATED
jobId: pushRef.key,
updates: BUNDLER_TASKS_DATABASE_PARENT + '/' + pushRef.key
});
})
.catch((err) => {
console.log('Failed to submit bundler task', err);
res.status(500).send('Failed to submit task')
});
}
/*
* HTTP Request function used to delete a download worker job
*
* Returns a JSON object with the created Job ID and a database location to listen to for updates.
*/
bundlerApp.post('/delete', (req, res) => {
if (!req.body.jobId) {
res.status(400).send('Missing jobId in body');
return;
}
let ref = admin.database().ref(BUNDLER_TASKS_DATABASE_PARENT + '/' + jobId)
ref.transaction((value) => {
if (value != null && value.data.requestedBy === req.user.uid && (value.status == 'ready' || value.status == 'error')) {
return null; // commit delete
} else {
return; // abort, already deleted or insufficient permission
}
})
.then((result) => {
if (!result.snapshot.exists()) {
res.status(200).send('Deleted');
} else if (result.snapshot.status == 'ready' || result.snapshot.status == 'error')) {
res.status(403).send('Unauthorized');
} else {
res.status(403).send('Unauthorized: In progress');
}
})
.catch((err) => {
res.status(500).send('Failed to delete task')
});
}
exports.bundler = functions.https.onRequest(bundlerApp);
/*
* Worker thread that downloads and pipes content to Cloud Storage
*/
exports.bundlerWorker = functions.database.ref(BUNDLER_TASKS_DATABASE_PARENT + '/{jobId}/data')
.onCreate((snaphot, context) => {
const path = snapshot.val().path;
const jobId = context.params.jobId;
const jobRef = snapshot.ref.parent;
return return jobRef.update({
status: 'started',
"data/startedAt": (new Date(context.timestamp)).getTime()
})
.then(() => jwtClient.request({ // uses gaxios
url: getDatabaseDataURL(path)
responseType: 'stream'
})
.then((responseStream) => {
let storageFileRef = admin.storage().bucket().file(jobId + ".json");
return new Promise((resolve, reject) => {
let writableStream = storageFileRef.createWriteStream();
responseStream.on('error', reject);
writableStream.on('error', reject); // may as well connect it
writableStream.on('finish', () => resolve(storageFileRef)); // fired by pipe() when it's done
responseStream.pipe(writableStream);
});
})
.then((storageFileRef) => storageFileRef.getDownloadURL())
.then((storageFileURL) => {
return jobRef.update({
status: 'ready',
"data/url": storageFileURL
});
})
.catch((err) => {
return jobRef.update({
status: 'failed',
"data/error": err.toString()
})
.then(() => Promise.reject('Job failed'));
});
});
// returns https://PROJECT_ID.firebaseio.com/rest${path}.json
function getDatabaseDataURL(path) {
if (!path) { throw new Error('path required') }
let base = admin.app().options.databaseURL;
return base + '/rest' + (path.charAt(0) === '/' ? path : '/' + path) + '.json';
const databasePath = '/path/to/data'
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment