Why this callable cloud function is failing with "app":"MISSING"? - node.js

I am calling a cloud function which runs a transaction, however it is returning an error to console which says:
Callable request verification passed {"verifications":{"auth":"VALID","app":"MISSING"}}
Googling it led me to App Check which is a new thing in Firebase. I am using React-Native firebase npm packages and following its documentation about App Check is extremely difficult due to lack of proper explanation and examples.
Below I have the code which I am trying to execute in the function:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const firestore_ = admin.firestore();
// const { CustomProvider } = require("#react-native-firebase/app-check");
const appCheckForDefaultApp = admin.appCheck();
const GeoPoint = admin.firestore.GeoPoint;
const FieldValue = admin.firestore.FieldValue;
const _geofirestore_ = require("geofirestore");
const GeoFirestore = _geofirestore_.initializeApp(firestore_);
exports.createNew = functions.runWith({
allowInvalidAppCheckToken: true // Opt-out: Requests with invalid App
// Check tokens continue to your code.
}).region('europe-west6').https.onCall(async (data, context) => {
try {
//Checking that the user calling the Cloud Function is authenticated
if (!context.auth) {
return "The user is not authenticated: " + context.auth;
// throw new UnauthenticatedError('The user is not authenticated. Only authenticated Admin users can create new users.');
}
const longitude = data.longitude;
const latitude = data.latitude;
const thirty_mins_old = data.thirty_mins_old;
const currenttime = data.currenttime;
const GeoFirestore_ = new _geofirestore_.GeoFirestore(firestore_);
const sfDocRef = GeoFirestore_.collection('mycollection')
.limit(1)
.near({ center: new GeoPoint(latitude, longitude), radius: 0.05 });
GeoFirestore.runTransaction((transaction) => {
const geotransaction = new _geofirestore_.GeoTransaction(transaction, new GeoPoint(latitude, longitude));
return geotransaction.get(sfDocRef._collectionPath).then((sfDoc) => {
...
});
});
} catch (error) {
if (error.type === 'UnauthenticatedError') {
throw new functions.https.HttpsError('unauthenticated', error.message);
} else if (error.type === 'NotAnAdminError' || error.type === 'InvalidRoleError') {
throw new functions.https.HttpsError('failed-precondition', error.message);
} else {
throw new functions.https.HttpsError('internal', error.message);
}
}
});
EDIT:
I am debugging the app so I am not working on production. Does debugging still requires this to be configured?

The log message you are seeing is not an error - it's informational.
On each request, your callable functions will verify any auth or appcheck token included in the request. When these tokens are not present, the execution is passed to your handler - it's your responsibility to handle requests with missing tokens if necessary. It looks like you are already handling the case for missing auth token.
When executing functions in your auth emulator, auth/appcheck tokens are minimally verified - i.e. they should be valid JWT token but we don't actually verify the signature to ensure that it's signed by Firebase Auth/AppCheck backend.
If your function is erroring in your development environment, I suspect that the error is elsewhere.

Related

Firebase function TypeError .once is not a function and onCreate not working

I was trying to deploy a function to Firebase to send notifications to all admin accounts when a new user signs up to the app, this is my current code:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
exports.newDoctorNotification = functions.database.ref("/doctors/{pushId}")
.onCreate((snapshot, context) => {
const newDoctorID = context.params.pushId;
const notificationContent = {
notification: {
title: "New Doctor",
body: "A new doctor just signed up! uid: " + newDoctorID,
icon: "default",
sound: "default",
},
};
const adminTokensRef = functions.database.ref("device_tokens/admin");
const tokens = [];
adminTokensRef.once("value", (querySnapshot) => {
querySnapshot.forEach((adminToken) => {
tokens.push(adminToken.val());
});
});
if (tokens.length > 0) {
return admin.messaging().sendToDevice(tokens, notificationContent)
.then(function(result) {
console.log("Notification sent");
return null;
})
.catch(function(error) {
console.log("Notification failed ", error);
return null;
});
}
});
I have tried many variations such as the get() function and on(), but all give me the same error, I was trying to check the docs on this but they only talked about database triggers so I'm not sure if normal retrieval can work or not.
EDIT:
I updated my code to reach the database node through the snapshot given in the onCreate event, and now it works, although I am facing another problem now, if I push a new doctor under the node "doctors" it doesn't call the function.. but if I hit "test" in the Google Cloud console and it calls the function I get "null" in my snapshot.val() and "undefined" in the newDoctorID above, whereas the snapshot.key gives "doctors", why is it not calling the onCreate function?

Correctly fetch authentication tokens server-side in a node.js React app hosted on Cloud Run

While not a front-end developer, I'm trying to set up a web app to show up a demo for a product. That app is based on the Sigma.js demo app demo repository.
You'll notice that this app relies on a graph which is hosted locally, which is loaded as:
/src/views/Root.tsx :
useEffect(() => {
fetch(`${process.env.PUBLIC_URL}/dataset.json`)
.then((res) => res.json())
.then((dataset: Dataset) => {...
// do things ....
and I wish to replace this by a call to another service which I also host on Cloud Run.
My first guess was to use the gcloud-auth-library, but I could not make it work - especially since it does not seem to support Webpack > 5 (I might be wrong here), the point here this lib introduces many problems in the app, and I thought I'd be better off trying the other way GCP suggests to handle auth tokens: by calling the Metadata server.
So I replaced the code above with:
Root.tsx :
import { getData } from "../getGraphData";
useEffect(() => {
getData()
.then((res) => res.json())
.then((dataset: Dataset) => {
// do even more things!
getGraphData.js :
import { getToken } from "./tokens";
const graphProviderUrl = '<my graph provider service URL>';
export const getData = async () => {
try {
const token = await getToken();
console.log(
"getGraphData.js :: getData : received token",
token
);
const request = await fetch(
`${graphProviderUrl}`,
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
const data = await request.json();
console.log("getGraphData.js :: getData : received graph", data);
return data;
} catch (error) {
console.log("getGraphData.js :: getData : error getting graph data", error);
return error.message;
}
};
tokens.js :
const targetAudience = '<my graph provider service base URL>'; // base URL as audience
const metadataServerAddress = "169.254.169.254"; // use this to shortcut DNS call to metadata.google.internal
export const getToken = async () => {
if (tokenExpired()) {
const token = await getValidTokenFromServer();
sessionStorage.setItem("accessToken", token.accessToken);
sessionStorage.setItem("expirationDate", newExpirationDate());
return token.accessToken;
} else {
console.log("tokens.js 11 | token not expired");
return sessionStorage.getItem("accessToken");
}
};
const newExpirationDate = () => {
var expiration = new Date();
expiration.setHours(expiration.getHours() + 1);
return expiration;
};
const tokenExpired = () => {
const now = Date.now();
const expirationDate = sessionStorage.getItem("expirationDate");
const expDate = new Date(expirationDate);
if (now > expDate.getTime()) {
return true; // token expired
}
return false; // valid token
};
const getValidTokenFromServer = async () => {
// get new token from server
try {
const request = await fetch(`http://${metadataServerAddress}/computeMetadata/v1/instance/service-accounts/default/token?audience=${targetAudience}`, {
headers: {
'Metadata-Flavor': 'Google'
}
});
const token = await request.json();
return token;
} catch (error) {
throw new Error("Issue getting new token", error.message);
}
};
I know that this kind of call will need to be done server-side. What I don't know is how to have it happen on a React + Node app. I've tried my best to integrate good practices but most questions related to this topic (request credentials through a HTTP (not HTTPS!) API call) end with answers that just say "you need to do this server-side", without providing more insight into the implementation.
There is a question with similar formulation and setting here but the single answer, no upvote and comments is a bit underwhelming. If the actual answer to the question is "you cannot ever call the metadata server from a react app and need to set up a third-party service to do so (e.g. firebase)", I'd be keen on having it said explicitly!
Please assume I have only a very superficial understanding of node.js and React!

Getting empty ctx.body on post requests in Koajs

I'm new to Koa.
I wrote a simple api server with it. I have used "koa-bodyparser" and i have added content-type: application/json to the request header, but i still get empty request body on post requests.
Could anyone please guide me?
this is my server.js
const Koa = require('koa');
const bodyParser = require('koa-bodyparser')();
const compress = require('koa-compress')();
const cors = require('#koa/cors')();
const helmet = require('koa-helmet')();
const logger = require('koa-logger')();
const errorHandler = require('./middleware/error.middleware');
const applyApiMiddleware = require('./api');
const { isDevelopment } = require('./config');
const db = require('./db/db');
const server = new Koa();
db.connectDB();
/**
* Add here only development middlewares
*/
if (isDevelopment) {
server.use(logger);
}
/**
* Pass to our server instance middlewares
*/
server
.use(errorHandler)
.use(helmet)
.use(compress)
.use(cors)
.use(bodyParser);
/**
* Apply to our server the api router
*/
applyApiMiddleware(server);
module.exports = server;
and this is my endpoint:
router.post('/', controller.createOne);
and the createone method:
exports.createOne = async ctx => {
console.log(ctx.body);
ctx.assert(username, 400, 'Username is required');
ctx.assert(password, 400, 'Password is required')
try {
const { name, username, password } = ctx.request.body;
let user = await User.findOne({ username });
if (user){
ctx.status = 400;
ctx.body = { errors: [{ msg: 'User already exists' }] };
}
user = new User({
name,
username,
password
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
user.password = undefined;
ctx.status = 201;
ctx.body = user;
} catch (error) {
console.error(error.message);
ctx.status = 500;
ctx.body = { errors: [{ msg: error.message }] }
}
};
and the postman request:
You are confusing ctx.body with ctx.request.body, at least in your log statement (in the destructured assignment it is actually correct).
ctx.body is the same as ctx.response.body, it's the response body which is empty because you didn't set it yet.
ctx.request.body is the request body which you actually want.
Some other issues I noticed:
You use username and password in those ctx.assert lines before they are defined.
In your duplicate user case, you forgot to return from your function, so the rest of the function will still run and even with an existing user you will create a new one.
Since you seem to be working on debugging that error 500: A little tip, error.message is quite useless for debugging as it's missing the stack (most importantly - since this is what shows you where exactly the error came from) and the error class, code and other properties. If you use console.error, always log the whole error object and not just the message: console.error(error). If you want to prepend some text, don't use concatenation, use separate arguments instead, so the object is still formatted: console.error('Error in request:', error).
If you need a string (for example when returning it as response, which you should do only in development mode by the way because you don't want to expose your inner workings to potential attackers), use error.stack and not error.message, because it will contain a lot more information.

Asynchronous Issue with createCustomerProfile in Sample Node.JS code

Our goal is to integrate Authorize.NET into our application using the Node SDK sample code.
Node SDK: https://github.com/AuthorizeNet/sdk-node
Recommended Sample Code: https://github.com/AuthorizeNet/sample-code-node/tree/ef9e5c2d9e0379b5f47a0ebcb6847e711fe196ef
I am trying to create a customer payment profile and while I am able to create a customer profile successfully and receive a successful response from the API call 
CustomerProfilesModule.createCustomerProfile, the remainder of my auth.controller.js runs before I get the API result. All of the create-customer-profile.js runs up until the ctrl.execute() runs, then the console.log("xxx") in auth.controller.js runs before grabbing the API result. 
I understand this is a synchronous issue with my code, but I don't know how to solve this. I am using the sample code authorize.NET provided, however the code is using the real data from my app rather than the sample data. I am more than happy to provide further information upon request and really appreciate any help! 
// AUTH.CONTROLLER.JS
const httpStatus = require("http-status");
const ApiContracts = require("authorizenet").APIContracts;
const ApiControllers = require("authorizenet").APIControllers;
const SDKConstants = require("authorizenet").Constants;
const User = require("../models/user.model");
const RefreshToken = require("../models/refreshToken.model");
const moment = require("moment-timezone");
const { jwtExpirationInterval } = require("../../config/vars");
const sgMail = require("#sendgrid/mail");
const bcrypt = require("bcryptjs");
const CustomerProfilesModule = require("../utils/authorizeNet/CustomerProfiles");
sgMail.setApiKey(process.env.SENDGRID_API_KEY.replace(/\r?\n|\r/g, ""));
exports.register = async (req, res, next) => {
try {
const userData = req.body;
let customerProfileResult =
await CustomerProfilesModule.createCustomerProfile(userData);
console.log(
"❌ ❌ ❌ ❌ ❌ customerProfile Result ",
customerProfileResult
);
if (!userData || userData.error) {
return next(error);
} else {
const { isTrial } = userData;
const user = await new User(userData).save();
const token = generateTokenResponse(user, user.token());
res.status(httpStatus.CREATED);
return res.json({ token, user });
}
} catch (error) {
console.log(error.message);
return next(User.checkDuplicateEmail(error));
}
};
//////////
// CREATE-CUSTOMER-PROFILE.JS
var ApiContracts = require("authorizenet").APIContracts;
var ApiControllers = require("authorizenet").APIControllers;
var utils = require("../utils.js");
async function createCustomerProfile(user) {
console.log(" user parameter", user);
var merchantAuthenticationType =
new ApiContracts.MerchantAuthenticationType();
merchantAuthenticationType.setName(process.env.AUTHORIZE_NET_API_LOGIN_KEY);
merchantAuthenticationType.setTransactionKey(
process.env.AUTHORIZE_NET_TRANSACTION_KEY
);
var creditCard = new ApiContracts.CreditCardType();
creditCard.setCardNumber(user.cardNumber);
if (user.cardExpiry.length > 4) {
creditCard.setExpirationDate(
`${user.cardExpiry.slice(0, 1)}${user.cardExpiry.slice(3, 4)}`
);
} else {
creditCard.setExpirationDate(user.cardExpiry);
}
console.log("creditCard", creditCard);
var paymentType = new ApiContracts.PaymentType();
paymentType.setCreditCard(creditCard);
var customerAddress = new ApiContracts.CustomerAddressType();
customerAddress.setFirstName(user.firstName);
customerAddress.setLastName(user.lastName);
customerAddress.setAddress(user.mailingAddress);
customerAddress.setCity(user.mailingCity);
customerAddress.setState(user.mailingState);
customerAddress.setZip(user.mailingZip);
customerAddress.setCountry("USA");
customerAddress.setPhoneNumber(user.userPhone);
var customerPaymentProfileType =
new ApiContracts.CustomerPaymentProfileType();
customerPaymentProfileType.setCustomerType(
ApiContracts.CustomerTypeEnum.INDIVIDUAL
);
customerPaymentProfileType.setPayment(paymentType);
customerPaymentProfileType.setBillTo(customerAddress);
var paymentProfilesList = [];
paymentProfilesList.push(customerPaymentProfileType);
console.log(
"paymentProfilesList",
paymentProfilesList
);
var customerProfileType = new ApiContracts.CustomerProfileType();
customerProfileType.setMerchantCustomerId(
"M_" + utils.getRandomString("cust")
);
customerProfileType.setDescription(
`${user.firstName} ${user.lastName}'s Account'`
);
customerProfileType.setEmail(user.userEmail);
customerProfileType.setPaymentProfiles(paymentProfilesList);
var createRequest = new ApiContracts.CreateCustomerProfileRequest();
createRequest.setProfile(customerProfileType);
createRequest.setValidationMode(ApiContracts.ValidationModeEnum.TESTMODE);
createRequest.setMerchantAuthentication(merchantAuthenticationType);
var ctrl = new ApiControllers.CreateCustomerProfileController(
createRequest.getJSON()
);
// All above code is ran when CustomerProfilesModule.createCustomerProfile(userData) is executed in auth.controller.js
// However the following line (line 130 in auth.controller.js) is ran before the below ctrl.execute() code is completed
//
// console.log("❌ ❌ ❌ ❌ ❌ customerProfile Result ", customerProfileResult);
//
// All the above code is executed before that console.log("❌ ❌ ❌") statement above, however the below code doesn't run before that console.log
// I'd like the below code to execute before the remaining register route is finished, but just don't know what is going on!
ctrl.execute(async function () {
var apiResponse = await ctrl.getResponse();
console.log("apiResponse", apiResponse);
var response = new ApiContracts.CreateCustomerProfileResponse(apiResponse);
console.log("response", response);
//pretty print response
//console.log(JSON.stringify(response, null, 2));
if (response != null) {
if (
response.getMessages().getResultCode() ==
ApiContracts.MessageTypeEnum.OK
) {
console.log(
"Successfully created a customer profile with id: " +
response.getCustomerProfileId()
);
} else {
console.log("Result Code: " + response.getMessages().getResultCode());
console.log(
"Error Code: " + response.getMessages().getMessage()[0].getCode()
);
console.log(
"Error message: " + response.getMessages().getMessage()[0].getText()
);
return {
error:
"Error message: " +
response.getMessages().getMessage()[0].getText(),
};
}
} else {
console.log("Null response received");
return { error: "Null response received" };
}
});
}
module.exports.createCustomerProfile = createCustomerProfile;
ctrl.execute is a method handling an IIFE function which is not called.
IIFE functions are invoked as soon as is defined.
You will not be able to run IIFE before declaration.
Possible solution:
Try to create a callback route inside register route and then exclude the IIFE to get the response from the callback inside register before actual route finished.
I don't fully understand your code enough to know how you want it to work. But from my understanding the console.log is running between when you get a call from the api and after you get the api, putting it in an awkward phase.
How asynchronous code works is that JavaScript will let the asynchronous function run, leave, do something else in the meantime, and get back to it when it is done.
The issue I see with your code is that createCustomerProfile doesn't return anything when it's done. You have a function that returns a promise of void. First off, that's a problem because you're using the return value of the function in console.log().
I highly recommend to promisify so that it properly resolves or has an error, which when you're working with API's you're likely to encounter potential errors in which you want to handle that.
You say the console.log() is being called before ctrl.execute() but I don't even see where it is being executed at all because I don't see it in the createCustomerProfile function.

Error upon Cloud Function for Firebase deployment

I've been trying to deploy a Cloud Function to my Firebase project.
It's my first time doing so, also my first time programming with JavaScript.
Here's my code in Node.JS:
'use strict'
const admin = require('firebase-admin');
const functions = require('firebase-functions');
admin.initializeApp(functions.config().firebase);
const firebaseTriggers = functions.region('europe-west1').firestore;
const db = admin.firestore();
exports.postNotification = firebaseTriggers
.document('/post notifications/{notificatioId}').onWrite((snap, context) => {
const notifcationRecieverId = snap.data().mReciever;
const payload = {
data: {
notification_type: 'POST',
title: snap.data().mTitle,
body: snap.data().mDescription,
sender_id: snap.data().mSender,
reciever_id: snap.data().mReciever,
notification_id: context.params.notificatioId
}
};
return db.collection('dog owners')
.document(notifcationRecieverId)
.get()
.then(recieverDoc => {
console.log('Retrieving FCM tokens');
const tokens = recieverDoc.data().mTokens;
console.log('Sending notification payload');
return admin.message().sendToDevice(tokens, payload);
});
});
Upong deployment, I'm getting the following error:
Can someone help me understand why?
Firstly you have got space in your colleciton name. This is bad convetion.
post notifications => postNotifications

Resources