RESTful API hosting on GCP - firebase

I have this multi layered application entirely hosted on GCP. At the moment, we only have the back-end part. Front-end and API are to be developed. For the front-end, the decision has been made - it will be a React.js app hosted on Firebase Hosting and the authentication method will be Email/password and users will be provisioned manually through the Firebase Hosting UI.
As we'd like to have a re-usable middle layer (API) we're in a process of making a decision what type of a solution to be used for our middle layer. The main request here is only logged in users to be able to call the API endpoints. Eventually, there will be also a native/mobile application which will have to also be able to make authenticated requests to the API.
My question here is, what type of GCP service is advised to pick here? I want it to be light, scalable and price optimized. Preferred programming language would be C# but Node.js would be also acceptable.

Firebase Functions would work well for this authenticated API. With a function, you can simply check for the existence of context.auth.uid before proceeding with the API call.
https://firebase.google.com/docs/functions/callable
You'll want to use the .onCall() method to access this context.auth object.
Here's an example I took from one of my active Firebase projects which uses this concept:
Inside your functions>src folder, create a new function doAuthenticatedThing.ts
/**
* A Firebase Function that can be called from your React Firebase client UI
*/
import * as functions from 'firebase-functions';
import { initializeApp } from 'firebase/app';
import { connectFirestoreEmulator, getFirestore, getDocs, query, where, collection } from 'firebase/firestore';
import firebaseConfig from './firebase-config.json';
let isEmulator = false;
const doAuthenticatedThing = functions
.region('us-west1')
.runWith({
enforceAppCheck: true,
memory: '256MB',
})
.https.onCall(async (_data, context) => {
// disable if you don't use app-check verify (you probably should)
if (context.app == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.',
);
}
// checks for a firebase authenticated frontend user
if (context.auth == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The user must be authenticated.',
);
}
// establish firestore db for queries
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// start the emulator
if (process.env.MODE === 'development' && !isEmulator) {
connectFirestoreEmulator(db, '127.0.0.1', 6060);
isEmulator = true;
}
// obtain the user's firebase auth UID
const uuid = context?.auth?.uid as string;
// do some database stuff
const ref = collection(db, 'collection-name');
const q = query(ref, where(uuid, '==', uuid));
const results = await getDocs(q);
if (results.empty) {
throw new functions.https.HttpsError(
'internal',
'There were no results found!',
);
}
// prepare document data
const data: Array<any> = [];
// gather chats, and an array of all chat uids
results.forEach((d) => {
data.push({ id: d.id, data: d.data() });
});
return data;
});
export default doAuthenticatedThing;
Make sure to reference this new Firebase Function in the functions/src/index.ts file.
import doAuthenticatedThingFn from './doAuthenticatedThing';
export const doAuthenticatedThing = doAuthenticatedThingFn;
Create a frontend React Hook so any component can use any function you make. Call it useGetFunction.ts
import { getApp } from 'firebase/app';
import { getFunctions, HttpsCallable, httpsCallable } from '#firebase/functions';
const useGetFunction = (functionName: string): HttpsCallable<unknown, unknown> => {
const app = getApp();
const region = 'us-west1';
const functions = getFunctions(app, region);
return httpsCallable(functions, functionName);
};
export default useGetFunction;
Now you can simply get this function and use it in any React component:
const SomeComponent = () => {
const doAuthenticatedThing = useGetFunction('doAuthenticatedThing');
useEffect(() => {
(async () => {
const results = await doAuthenticatedThing();
})();
}, []);
};

Related

Mixed up data on concurrent requests to vue3 SSR app with apollo client

I'm trying to convert a vue3 SPA that uses an apollo client to SSR with the help of vite-plugin-ssr.
I'm facing an issue of mixed-up data on concurrent requests to the express server. The apollo client instance used to be global in the SPA, so I'm trying to create and use a new instance on every request.
I had this file that included the creation of the client, and wrapper functions around the client functions :
// apollo.ts - SPA
const apolloClient = new ApolloClient({
...
})
export const apolloQuery = async (args) => {
// some logic
const response = await apolloClient.query({...})
// some logic
return response
}
...
Those functions are used in components and store.
The integration example of apollo in vite-plugin-ssr repo uses #vue/apollo-composable
So I tried :
// app.ts
import { DefaultApolloClient } from "#vue/apollo-composable";
import { createApolloClient } from "./path/to/apollo.ts";
const createApp = () => {
const apolloClient = createApolloClient({...})
const app = createSSRApp({
setup() {
provide(DefaultApolloClient, apolloClient)
},
render() {
return h(App, pageProps || {});
}
});
}
// apollo.ts - SSR
import { useApolloClient } from "#vue/apollo-composable"
export const createApolloClient = (args) => {
// some logic
return new ApolloClient({...})
}
export const apolloQuery = async (args) => {
// some logic
const { client } = useApolloClient()
const response = await client.query({...})
// some logic
return response
}
However, this does not work as intended because useApolloClient depends on providing the apollo instance to the app (1), or using the provideApolloClient function (2) :
(1) this means that calls can only be made during component setup or life cycle hooks, as it leverages the Vue core function getCurrentInstance to inject the client. So I can neither use them in the onBeforeRender that vite-plugin-ssr provides, nor in Vue onServerPrefetch hook, nor in the async call to store action.
(2) provideApolloClient sets the client to a global variable, and the same problem occurs on concurrent requests.
Am I at a dead end and a complete refactor of the apollo client use is needed, or am I missing something?

How to make Firestore query during SSR

I would like to fetch data from Firestore during server side rendering. I know I could use REST API (and attach the token to the request's headers) but I don't want to write REST requests on server side and then duplicate the same requests on client side using standard Firestore queries. On client I prefer standard queries (no REST) because of the realtime updates. And I would like to reuse the queries from client also on the server (even without the benefit of realtime updates).
I validate the token manually on the server:
import admin from 'firebase-admin';
import { initializeApp, getApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseApp = initializeApp(config);
const db = getFirestore(firebaseApp);
const decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie);
// => token verified: decodedIdToken.userId = "xxx"
But when I want to perform a query:
import { collection, getDocs } from 'firebase/firestore';
const querySnapshot = await getDocs(collection(db, 'myCollection'));
I get error:
{
"code": "permission-denied",
"name": "FirebaseError"
}
Firestore rules:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
On client side the error could be solved using onAuthStateChanged but I can't use this listener on server.
Is there any way to run Firestore queries with manually verified token?
I've managed to replicate your error. You're getting this error because you're trying to use client SDK instead of firebase-admin.
Here's the sample code for your reference:
import admin from 'firebase-admin';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from "firebase-admin/firestore";
const firebaseApp = initializeApp(config);
const db = getFirestore(firebaseApp);
// const decodedIdToken = await admin.auth().verifySessionCookie(sessionCookie);
// => token verified: decodedIdToken.userId = "xxx"
// const querySnapshot = await getDocs(collection(db, 'myCollection'));
db.collection("myCollection").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
});
I've used version 8 (commonJS) instead of version 9 (modular) as firebase-admin still uses the dot notation syntax.
[sampleQuery] => { test: 'testing' }
Here's the link on how to get all documents in a collection.
Here's another reference on upgrading to Node.js SDK Admin SDK v10 (modular SDK).
Update:
If you wanted to use the Firestore Security Rules, you need to use custom signed tokens, you need to pass it to signInWithCustomToken so that the client auth can sign in.
Below is a sample code for your reference:
import { getAuth, signInWithCustomToken } from "firebase/auth";
const auth = getAuth();
signInWithCustomToken(auth, token)
.then((userCredential) => {
// Signed in
const user = userCredential.user;
// ...
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
// ...
});
A new user will be created and linked all their credentials and the new account will be stored as part of your project, and will be used to identify a user across every app in your project.
You can also allow a user to sign out by calling signOut:
import { getAuth, signOut } from "firebase/auth";
const auth = getAuth();
signOut(auth).then(() => {
// Sign-out successful.
}).catch((error) => {
// An error happened.
});
You can check this documentation on authenticating with Firebase for additional information.

How to update two databases reference with a single trigger function in firebase RTDB?

Let's say we have firebase project in which we have to use RTDB.
In RTDB we have created multiple databases.
I created a cloud trigger function i.e .onCreate so that my both databases get updated whenever I update any of two. When I am creating anything in default database it is working completely fine but when I am trying to update through other database (other than default one) it doesn't update default one. So could you please help me on this?
/* eslint-disable */
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
//this method is updating on creating data on database mentioned in instance id
export const newTest1=functions.database.instance('flysample-75b81-227ae').ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
//this method is updating only by creating data on default database
export const newTest2=functions.database.ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
//below 2 method works fine but i want to do this by single function
export const myFunTest1 = functions.database.instance('flysample-75b81').ref('/name')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
export const myFunTest2 = functions.database.instance('flysample-75b81-227ae').ref('/name')
.onCreate((snapshot, context) => {
let app = admin.app();
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
return "done";
});
Your code is completely ignoring the asynchronous nature of writing to the database, which means there is no guarantee that any of the database writes completes before the instance gets terminated.
To ensure the writes don't get interrupted, wait for them to complete before returning a result with something like this:
export const newTest2=functions.database.ref('/msg')
.onCreate((snapshot, context) => {
let app = admin.app();
return Promise.all([
app.database('https://flysample-75b81.firebaseio.com/').ref('/db1').set({Name:"Database1"})
app.database('https://flysample-75b81-227ae.firebaseio.com/').ref('/db1').set({Name:"Database1"})
]).then(() => {
return "done";
});
});

connecting to firestore emulator with #firebase/testing

I am trying to test a firebase app locally.
I am running the test with firebase emulators:exec --only firestore 'mocha -r ts-node/register src/**/*.spec.ts
In my spec, I import #firebase/testing and setup my app and followed the directions from
https://firebase.google.com/docs/rules/unit-tests
I have a FirebaseService which is a singleton wrapper for my methods into which I inject my firebase app.
In production, I'll inject the firebase, and it gets initialized in the FirebaseService in testing, I initialize outside of the service.
The wrapper is fairly simple
export const FirebaseService = (function(): FirebaseSrvc {
let firebase;
const fbServiceObj: FirebaseSrvc = {
getInstance: (firebaseConfig, firebaseCore, initialize) => {
firebase = firebaseCore;
if (initialize && firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig);
}
return fbServiceObj;
},
createActivity: async (title: string) => {
try {
const firebaseUid = firebase.auth().currentuser.uid;
const newActivity: ActivityProps = {
title,
created_at: 123445,
created_by: firebaseUid,
public: false,
available_to: [firebaseUid],
};
console.log(' before create', newActivity);
const createResponse = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.set(newActivity);
console.log('create response', createResponse);
return true;
} catch (e) {
console.log('error creating activity', e);
}
},
getActivity: async (title: string): Promise<ActivityProps> => {
try {
const actResponse: DocumentReferenceTo<ActivityProps> = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.get();
return actResponse as ActivityProps;
} catch (e) {
console.log('error getting activity from firebase', e);
}
},
};
return fbServiceObj;
})();
The test I am attempting to run is
import * as firebase from '#firebase/testing';
import { assert } from 'chai';
import 'mocha';
import * as appConfig from '../../app-dev.json';
import { FirebaseService } from '../services/FirebaseService';
firebase.initializeTestApp({ ...appConfig.expo.extra.firebase, auth: { uid: 'random', email: 'test#test.com' } });
describe('Activity', async () => {
const fb = FirebaseService.getInstance(appConfig.expo.extra.firebase, testApp, false);
const activityData = new Activity(fb);
beforeEach(async () => await firebase.clearFirestoreData({ projectId }));
it('should create a new activity', async () => {
await activityData.set('test-activity'); // this runs FirebaseService.createActivity
const findActivity = await activityData.get('test-activity'); // this run FirebaseService.getActivity
assert(findActivity.title === 'test-activity');
});
});
When I run the test I get an error
Your API key is invalid, please check you have copied it correctly.] {
code: 'auth/invalid-api-key',
message: 'Your API key is invalid, please check you have copied it correctly.'
}
I can confirm that the API key which is passed into firebase.initializeTestApp matches the Web API Key in my firebase console.
I have also downloaded the google-services.json from my firebase console and lists
{
"api_key": [
{ "current_key": different_from_web_key}
]
}
And I have replaced my existing key with this new key, I still get the same error.
I have also tried setting up initializeTestApp({ projectId }) which is how the example from firebase docs sets it up, and I receive the same result.
I am using the same project details to run a project locally in android studio, and I am able to authenticate and write to firestore, so the API key I am using does work, but it appears to have issues being used in the test app.
This usually doesn't have a specific way to solve it. It might be that even a new copy and paste of the API key to the parameters, might make it work and the error to disappear.
I would recommend you to take a look at the following posts from the Community, that have some possible fixes for the error that you are facing.
Firebase Error: auth/invalid-api-key, Your API key is invalid, please check you have copied it correctly
Invalid API Key supplied using Firebase
In addition to that, since Firebase has free support offers, I think you reaching out to the Firebase support would help you fix this quickly. You should be able to contact directly for free.
Let me know if the information helped you!

Get current users access token from Firebase in React Native

I am trying to get the Firebase authentication access token within a React Native application so that I can authenticate my API calls to a custom server. The Firebase documentation says I should get this token by using auth().currentUser.getIdToken(); however currentUser returns null.
I've tried to use getIdToken() in multiple areas of the application. I know the access token is generated as I can see it in the logs while using expo (user.stsTokenManager.accessToken).
Why is currentUser returning null and how can I get the accessToken?
You need to wrap user.getIdToken() inside of firebase.auth().onAuthStateChanged for user to be available. You can then use jwtToken in your header to authenticate your API calls. You need to import your Firebase configuration file for this to work.
let jwtToken = firebase.auth().onAuthStateChanged(function(user) {
if (user) {
user.getIdToken().then(function(idToken) { // <------ Check this line
alert(idToken); // It shows the Firebase token now
return idToken;
});
}
});
Just putting await before will work too just like this:
await auth().currentUser.getIdToken();
getIdToken returns a promise
firebase.auth()
.signInWithCredential(credential)
.then(async data => {
const jwtToken = await data.user?.getIdToken();
console.log(jwtToken);
})
Hook example
Unfortunately, its not reliable to directly get the token. You first have to listen to the authentication state change event which fires upon initialization since its asynchronous.
import {auth} from '../utils/firebase'
import {useState, useEffect} from 'react'
export default function useToken() {
const [token, setToken] = useState('')
useEffect(() => {
return auth().onAuthStateChanged(user => {
if (user) {
user.getIdToken(true)
.then(latestToken => setToken(latestToken))
.catch(err => console.log(err))
}
})
}, [])
return token
}
then use like so in your functional component
const token = useToken()
useEffect(() => {
if (token) {
// go wild
}
}, [token])

Resources