Firestore email attachments - firebase

Basically what I'm looking for is the ability to send emails with attachments (multiple attachments) to my email. My current approach is to get files and convert them to the dataUri then save email skeleton to firestore. Then I have a cloud function that fires onSave and sends the email to my email using Gmail transporter and after I delete the record from firestore so there is no persistent saving to a database. This approach works but its very limited because I can't send files which sum exceeding 1 MB and that's sucks.
Can anyone point me the best way to get similar behaviour but be able to send larger files? I would like to send up to 5 files with 3mb per file if its possible.
P.S.
I've enabled blaze plan
Im not sure whats wrong here.. Im unable to send an email.
Yeah, right I forgot about HTTPS functions. But I've one problem Im unable to send an email and Im not sure what's wrong.
exports.sendEmaill = functions.https.onRequest((req, res) => {
if (req.method !== 'POST') {
return res.status(403).send('Forbidden!');
}
console.log(req.body);
return cors(req, res, () => {
const mailOptions = parseBody(req.body);
console.log(mailOptions);
// return res.status(200).send(mailOptions)
return mailTransport.sendMail(mailOptions)
.then((info) => res.status(200).send(info))
.catch((error) => res.status(403).send(error));
});
});
With this I can fire the function
// return res.status(200).send(mailOptions)
But as soon as I want to use mailTransport I got
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Related

Securing Firebase Cloud Function HTTP Endpoint

I have a pretty simple use-case for a front facing website. It has a contact form and the details of the contact form need to be saved to the firebase database for further processing. The website is built using NextJS. I understand that the api functionality of NextJS is not usable using Firebase Hosting so as a result, I'm inclined to use Cloud Functions to set up a HTTP endpoint that accepts form data as a POST request and save it to the realtime database / Firestore.
However, I'm unable to figure out a way to secure this endpoint. How do I prevent a normal user from extracting the endpoint URL from website source code and sending multiple requests to that URL? Can I keep this endpoint responsive only for that particular domain? Or how do I resolve this?
Alternatively, I could use the Firebase SDK directly in-app and save the data to the database but that would require me to keep the contacts collection as public for anyone to read/write, which is again a security risk.
What would be a better way to solve this issue whilst keeping the security intact? Note that since its a public website, I cannot have authenticated users with Firebase.
That is not possible. Users can see all the URLs they are making calls to in the networks tab. Your serverless functions must be ready to handle spam (like reject malicious or badly formatted requests), though you will still be charged for the CPU usage. That is one of the biggest cons of being serverless. But you will be saving a lot of time setting up servers and all that hassle.
The best you can do is enable CORS which still won't prevent spam but will reject requests after the pre-flight request. Though only browsers follow CORS and API clients like postman or insomnia don't
This cannot be considered as a security threat as it all depends on your code's logic, however you'll be charged for the usage and that's the risk involved. There are services like Cloudflare API Shield but again, Firebase has it's own SDK so that can be bypass somehow.
Coming to the reCaptcha case which involves verifying the reCaptcha token on the backend, you may get rid of bots to some extents. But if someone just keeps spamming your server without valid token, your functions are still going to charge you for time taken for validating the token.
Please let me know if you have any more questions.
Both of your options can work.
Using the rest api with clloud functions you could integrate the Google Captcha.
Using directly the database you can write the database rules in a way that everyone can only add a new contact and can't read or edit it. This is still less secure because someone could fill up your database. But with a quite good field validation and duplicate restriction that would be a "lesser" problem.
Here is how we did it with our website:
The cloud functions that handle the captcha and contact POST:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const rp = require("request-promise");
const nodemailer = require("nodemailer");
const gmailEmail = encodeURIComponent(functions.config().gmail.email);
const gmailPassword = encodeURIComponent(functions.config().gmail.password);
const mailTransport = nodemailer.createTransport(
`smtps://${gmailEmail}:${gmailPassword}#smtp.gmail.com`
);
exports.checkRecaptcha = functions
.region("europe-west1")
.https.onRequest((req, res) => {
const response = req.query.response;
console.log("recaptcha response", response);
rp({
uri: "https://recaptcha.google.com/recaptcha/api/siteverify",
method: "POST",
formData: {
secret: "TOP_SECRET",
response: response,
},
json: true,
})
.then((result) => {
console.log("recaptcha result", result);
if (result.success) {
res.send("You're good to go, human.");
} else {
res.send("Recaptcha verification failed. Are you a robot?");
}
})
.catch((reason) => {
console.log("Recaptcha request failure", reason);
res.send("Recaptcha request failed.");
});
});
exports.triggerEmail = functions
.region("europe-west1")
.https.onRequest((req, res) => {
if (req.method !== "POST") {
res.status(400).send("Please send a POST request");
return;
}
const values = JSON.parse(req.body);
const email = "email#company.com";
const bcc = "email#company.com";
const mailOptions = {
subject: "Kontakt von Website",
text: `Datum: ${values.dateTime}\n
Name: ${`${values.gender} ${values.firstname} ${values.lastname}`}
Firmenname: ${values.company ? values.company : ""}
Strasse: ${values.street ? values.street : ""}
Ort: ${values.place ? values.place : ""}
Land: ${values.country ? values.country : ""}
PLZ: ${values.zip ? values.zip : ""}
E-Mail: ${values.email ? values.email : ""}\n\n
${values.text ? values.text : ""}`,
to: email,
bcc: bcc,
};
console.log(req.headers.origin);
if (
req.headers.origin == "https://www.your_company.com" ||
req.headers.origin == "http://localhost:3000"
) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}
return mailTransport.sendMail(mailOptions).then(() => {
res.status(200).send("OK");
});
});
The Captcha in our side usinge react:
<ReCAPTCHA
ref='recaptcha'
sitekey='SECRET_KEY'
onChange={response => this.setState({ response: response })}
/>
And the contact POST call:
fetch('https://URL_TO_YOUR_FUNCTION_THAT_SENDS_THE_EMAIL', {
method: 'POST',
body: JSON.stringify({
dateTime: new Date().toString(),
gender: this.state.gender,
firstname: this.state.firstname,
lastname: this.state.lastname,
company: this.state.company,
street: this.state.street,
place: this.state.place,
country: this.state.country,
zip: this.state.zip,
email: this.state.email,
text: this.state.text,
isLogistik: isLogistik
})
})
Make sure that your Captcha settings are setup to us the second cloud function for verification.

How do I listen to email verification event with firebase authentication and react native?

EDIT - this question is still unanswered. There was an idea to listen to onIdTokenChanged, however the token is refreshed once every hour, which is not practical solution for me. I posted follow up question here if people can give me a hand that would be grant, because I am sitting on this problem since one week.
I am writing a simple react native app, and I want to show my main page only after user has verified their email. As far as I understand, there is no listener which I can use to listen to event where the user has been verified their email. I am using firebase.auth().onAuthStateChanged((user) => {....}) but the listener onAuthStateChanged has been called after user is logged in or registered in, not after a user has verified their email.
Few places suggested to use firebase.auth().user.reload() so that it will reload the current user and it will pick up the verification status from the database. However, I dont think it is a solution because I dont know when should I reload the current user, i.e. I dont know when the verification link has been clicked. So possible solution to this problem would be:
Send a confirmation 6 digit code to the user, and wait for the user to type it in the app; after the user types it, if the code is the same, I refresh the user. However I dont know how to send custom verification emails with firebase. I checked the documentation, but it is not helpful for me. If someone can point me to example written in react native, or write a small working example with custom email which I can send to the user (again in react native) that would be grant! EDIT - this doesn't seem like possible solution, since Firebase doesn't let you customize the emails
Is it possible solution for me to override onAuthStateChanged listener? S.t. it will listen for changes if the user's email has been verified or not? If that's a good idea can someone point me to the current onAuthStateChanged implementation in react-native, so I can see it as an "inspiration" when overriding? Or if someone has done something similar before, can you show me an example?
I've read several suggestions to use a deep link and to intersept the event when the link has been clicked, but I am not sure how to do this, or even if this is a proper solution to the problem.
Some people suggested to use firebase.auth().user.reload() when the app has been closed and reopened again. This comes from the assumption that when a user has been sent the link, in order for them to click on the link, they need to close the app, and reopen it again. I think this is pretty strong assumption, considering the fact, that they might verify their email via laptop and never close the app, so I dont think this is a good solution either.
Apparently this seems like a well known problem, yet there are not many good solutions. I think best possible solution would be to send 6 digit verification code to the user and after that code has been confirmed, I would reload the current user, pick up the emailVerified field, it will be set to true and then I will show the main screen. However, can someone help me with how do I send custom email in react native and firebase?
If anyone has any other suggestions, please let me know!
You can simply do this by passing a continue url in the actionCodeSettings as below:
const res = await firebase.auth().createUserWithEmailAndPassword(
email,
password
);
await res.user.sendEmailVerification({
url: "https://yoursite.com/continue-url"
});
Is it possible solution for me to override onAuthStateChanged listener? S.t. it will listen for changes if the user's email has been verified or not?
The onAuthStateChanged is called when the user's authentication state changes, so when they go from not being signed in to being signed in or vice versa. The email verification flag being set is not a change in authentication state, so the callback is not called in that case.
You can listen for onIdTokenChanged instead, which fires every time the ID token changes. Since the ID token includes the flag whether the user's email is verified, a callback on onIdTokenChanged will also be called when that changes.
I used #1man solution, just i make sure to delete the interval and unsubscribe from the onAuthStateChanged event:
const onAuthStateChangedUnsubscribe =
firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
// -> Alert Email Verification
await user.sendEmailVerification()
const onIdTokenChangedUnsubscribe = firebase.auth().onIdTokenChanged((user) => {
const unsubscribeSetInterval = setTimeout(() => {
firebase.auth().currentUser.reload();
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true)
}, 10000);
if (user && user.emailVerified) {
clearInterval(unsubscribeSetInterval) //delete interval
onAuthStateChangedUnsubscribe() //unsubscribe onAuthStateChanged
// -> Go to your screnn
return onIdTokenChangedUnsubscribe() //unsubscribe onIdTokenChanged
}
})
}
})
So, on my project I made a combination of sendEmailVerification() and reload().
Try it:
await firebase
.auth()
.currentUser.sendEmailVerification(actionCodeSettings)
.then(() => {
//useState used on my loading (user can cancel this loading and exit this task
setTextContent('Waiting for verification. Check your email!\nYou can close this verification and came back later');
const unsubscribeOnUserChanged = firebase
.auth()
.onUserChanged(response => {
const unsubscribeSetInterval = setInterval(() => {//this works as a next in for-like
firebase.auth().currentUser.reload();
}, 30000);
if (response.emailVerified) {
clearInterval(unsubscribeSetInterval); //stop setInterval
setLoading(false); //close loading describes above
navigation.goBack(); //return to parent (in my case to profile)
return unsubscribeOnUserChanged(); //unsubscribe onUserChanged
}
});
})
.catch(error => {
setLoading(false);
setError(true);
errorHandle(error);
});
#3 is a common workflow - Firebase sends the link which, when clicked, opens your app. Your app reads the deep link and handles the payload (email verified). I don't know what language you're using, but you mentioned that you don't know how to do this and it's probably something you'll want to explore.
Your concern in #4 (someone opening the link on a laptop) is only an issue if you allow it to be one. I don't know what language you're using, but when you call the verify email function, you have to pass a url to Firebase which it will use in the email it sends. So your users will be taken wherever you send them. If you send them to a web app or something because you want them to open it on a laptop, then I think your best bet in app would be to have your website (or wherever you're sending them) also write something to a Firestore or RTDB document and have your app listening to that doc for updates.
If the link you pass to Firebase is a deep link to your app, it won't work on their laptop. And in this case, you go back to #3 - read the deep link in your app and handle it early. Also, it's incumbent on you to explain to users how this works, so I'd have my send link confirmation screen explain that they should click the link on the current device.
An alternative would be to have your send link function in-app start a background timer that polls the auth record every few seconds/minutes (whatever your use case), and cancel it when the record is updated or the link expires. I don't love this because email links are valid for 3 days - that's an awful long time to be polling every few seconds in app.
I wanted to do the same thing on the web. I tried the previous three answers and searched a lot but was not able to find the answer. I ended up combining #Frank van Puffelen and #Hermanyo H's solutions into one and it worked for me:
const onAuthStateChangedUnsubscribe = firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
setEmailVerified("Sent");
await user.sendEmailVerification();
const onIdTokenChangedUnsubscribe = firebase.auth().onIdTokenChanged((user) => {
if (user && user.emailVerified) {
setEmailVerified("Verified");
return onIdTokenChangedUnsubscribe(); //unsubscribe
}
setTimeout(() => {
firebase.auth().currentUser.reload();
firebase.auth().currentUser.getIdToken(/* forceRefresh */ true);
}, 10000);
});
}
});
I wrote my own events instead of using onAuthStateChange.
//Write this where you wrote onAuthStateChange event
import auth from '#react-native-firebase/auth';
import {DeviceEventEmitter} from 'react-native';
useEffect(()=>{
let loginListener = DeviceEventEmitter.addListener('#verified_login', params=>{
setUserDetails(auth()._user);
});
return loginListener;
}, []);
Then you can emit this event when you want to allow the user to log in. There's a lot of room for customization here.
await auth().signInWithEmailAndPassword(email, password);
if(auth()._user.emailVerified)
DeviceEventEmitter.emit('#verified_login');
else{
auth()._user.sendEmailVerification()
.then(()=>{
console.log('A verification link has been sent to your email. Please verify to proceed.');
let emailVerificationEventListener = setInterval(async ()=>{
auth().currentUser.reload();
if (auth().currentUser.emailVerified) {
clearInterval(emailVerificationEventListener);
DeviceEventEmitter.emit('#verified_login');
}
}, 1000);
})
.catch(error=>{
console.log(error);
});
}
The api seems to have changed, this worked for me.
auth.idTokenResult.subscribe((result) => {
console.log('onIdTokenChanged');
console.log(result);
})
This issue can be fixed smoothly using firebase dynamic links
when a user requests to authenticate their emails we send a dynamic link with the request:
auth().currentUser.sendEmailVerification({
url: "https://oursite.com/verified-email",
});
when the user clicks on the link in the email he will be redirected to the dynamic link we included above
then we listen to the link and handle it on the client:
dynamicLinks().onLink((link) => {
if (link.url.includes("verified-email")) {
auth().currentUser.reload();
}};
Did you consider the documentation on the Firebase documentation pages?
https://firebase.google.com/docs/auth/web/email-link-auth
Sample code on that page:
import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth";
// Confirm the link is a sign-in with email link.
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
// Additional state parameters can also be passed via URL.
// This can be used to continue the user's intended action before triggering
// the sign-in operation.
// Get the email if available. This should be available if the user completes
// the flow on the same device where they started it.
let email = window.localStorage.getItem('emailForSignIn');
if (!email) {
// User opened the link on a different device. To prevent session fixation
// attacks, ask the user to provide the associated email again. For example:
email = window.prompt('Please provide your email for confirmation');
}
// The client SDK will parse the code from the link for you.
signInWithEmailLink(auth, email, window.location.href)
.then((result) => {
// Clear email from storage.
window.localStorage.removeItem('emailForSignIn');
// You can access the new user via result.user
// Additional user info profile not available via:
// result.additionalUserInfo.profile == null
// You can check if the user is new or existing:
// result.additionalUserInfo.isNewUser
})
.catch((error) => {
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
}

Push Notifications in Perl

I am implementing push notifications for a site that has a Perl back end. Firebase is the push notification service i am using. I have spent a fair bit of time with this and looked at a number of guides and some useful resources on SO. I have come up with a working implementation with just one issue. The problem is when send out a push notification it seems to arrive on the client/browser as an empty message. That is no data containing the 'title' and 'body' is retrievable on the client/browser side when the push notification arrives.
I have tried using both firebases older and newer api and either way it ends up with the same outcome of empty push notifications arriving on the client/browser. I have tested this on chrome,firefox and android and the same thing happens.
Here is the perl code that sends the push notification. I have excluded generating the bearer token to limit how much code there is to read.
#SEND PUSH NOTIFICATION
my $push_subscriber = <get subscriber details from db>
my $end_point_host = $push_subscriber->{endpoint};
my $end_point = "https://$end_point_host/v1/projects/<my project
id>/messages:send";
my $request = HTTP::Request->new('POST',$end_point);
$request->header('Authorization'=>"Bearer $bearer_token");
$request->header('Content-Type' => 'application/json');
$request->content(JSON::encode_json ({
message => {
token => $push_subscriber->{subscription_id},
notification => {
title => 'test',
body => 'test content'
},
webpush => {
headers => {
Urgency => 'high'
},
notification => {
body => 'test content',
requireInteraction => 'true'
}
}
}}));
#send the request
$ua->request($request));
Here is the client/browser side javascript that is called when a push notification arrives. This is inside service-worker.js
self.addEventListener('push', function(e) {
var body;
if (e.data) {//THE PROBLEM IS HERE. No 'data' object exists
body = e.data.text();
} else {
body = "Empty Message";
}
var options = {
body: body
};
e.waitUntil(
self.registration.showNotification('My Notification', options)
);
});
The point where the problem presents itself is pointed out in the above javascript. Any help/feedback would be much appreciated. Thanks.
I ended up getting this working by just re-writing my client side subscription code. In my case the bell icon subscription on/off button along with all the js code to make it work.
Basically i went from using googles solution to a firebase specific solution with this guide.
https://firebase.google.com/docs/cloud-messaging/js/receive
You only need to store the 'token' on your server and the endpoint is always - https://fcm.googleapis.com/v1/projects/YOUR PROJECT ID/messages:send
The firebase guide contains a sample file where you can subscribe/unsubscribe for push notifications.
https://github.com/firebase/quickstart-js/blob/4be200b1c55616535159365b74bfd1fc128c1ebf/messaging/index.html
Once i had this working i could then cut it down and re-write it into just a simple notification button.
For some reason the provided firebase-messaging-sw.js from the guide didn't work for me but using service-worker.js shown in my OP did and so i can now receive push notifications along with their title, body and other data.
This here is how i generate the bearer token used in my OP sample perl code to send out a push notification.
Google API OAuth2 : “invalid_grant” error when requesting token for Service Account with Perl
That should hopefully cover everything you need to know if you are wanting to do push notifications on a site with a Perl back end. Hopefully this helps someone else wanting to do the same thing.

How to make asynchronous calls from external services to actions on google?

I'm trying to connect Google Home to an external chatbot with actionssdk. I have an API that take user inputs and send them to my chatbot with webhook, but my chatbot make a response calling another endpoint of my API in an async way, and I can't show the response in actions on Google or Google Home.
I create an actionssdkApp.
const {
actionssdk,
SimpleResponse,
Image,
} = require('actions-on-google');
var app = actionssdk();
var express_app = express();
My API has 2 endpoints. One of them is for actions on google to send user inputs to my chatbot:
app.intent('actions.intent.MAIN', conv => {
console.log('entra en main');
conv.ask('Hi, how is it going?');
});
app.intent('actions.intent.TEXT', (conv, input) => {
var userId = conv.body.user.userId;
console.log(userId);
if(userId && input){
textFound(conv, input, userId);
}else{
textnotFound(conv);
}
});
TextFound function send user inputs to my chatbot with webhook, but the request doesn't receive the response. My chatbot call another endpoint with the text answer:
express_app.post('/webhook', bodyParser.json(), (req, res)=>{
console.log("Webhook");
const userId = req.body.userId;
if (!userId) {
return res.status(400).send('Missing User ID');
}
console.log(req.body);
res.sendStatus(200);
});
And here is where I want to send the answer to Google Home. But I need the conv object to show the answer in google Home, or actions on google, or any other device.
Edit:
My textFound function:
webhook.messageToBot(metadata.channelUrl, metadata.channelSecretKey, userId, input, function(err){
if(err){
console.log('Error in sending message');
conv.ask("Error in sending message");
}else{
conv.ask("some text");
}
});
From here my api send user inputs to my bot through messageToBot function:
request.post({
uri: channelUrl,
headers: headers,
body: body,
timeout: 60000,
followAllRedirects: true,
followOriginalHttpMethod: true,
callback: function(err, res, body) {
if (err) {
console.log('err: '+err);
callback(err);
} else {
console.log('Message sent');
callback(null);
}
}
});
From now on, my bot doesn't send a response but makes a call to /webhook endpoint of my api with the answer. But in this function I haven't de conv object and I can't send the answer to google. I don't know how to access to this object. Maybe there is an uri to connect with my project in actions on google from my api.
Typically, Actions on Google works in a request-response way. The user says something to the Action, and the Action replies with a response. That reply needs to come within about 5 seconds. If you think the call to /webhook can come that quickly, and you will only deliver a message to the user after they say something, you can have /webhook save the response in a queue for the user, and have your Intent handler be in a loop that checks this queue for any messages to reply with - if there is a message within 5 seconds, you reply with it, if not, you need to reply before the 5 seconds are up.
If you can't guarantee it will be done within 5 seconds, however, there are a couple of workarounds that might be useful depending on your needs.
The first is that you might be able to use notifications. In this scenario, you would send the message from the user and then close the conversation. When your /webhook endpiont is triggered, you would locate the user and send the notification to their Assistant. Unfortunately, this is a bit bulky, doesn't lead to a very interactive chat system, and notifications also aren't supported on smart speakers.
You can also look into using a Media Response to set up a way for you to poll for new messages periodically. Under this scheme, your user would send their message. In your reply to them, you would include a Media Response for some audio that plays for, say, 15 seconds. When the audio finishes, your Action will be called again and you can check to see if any messages have been queued up to be delivered to the user. If so, you relay those messages, followed by a Media Response gain. Otherwise, just send a Media Response. Your call to /webhook would have to put messages in a queue to be delivered to the user. This is more complex, especially to scale, but can be made more interactive. It is also a more general case of trying to handle it in a loop inside 5 seconds.

How do I write a http triggered Firebase cloud function which registers users with input info from client

I am working on system admin part of my project which means that an already authenticated user should be able to register new accounts.
I have learned that this is possible with using Firebase cloud functions. I guess I need to send input values with POST trigger to function, then function will use this data to register user.
I read the documentation but couldn't find enough info about http POST request.
Any help on this would be appreciated.
It's pretty simple. You can retrieve request body as below.
exports.helloWorld = functions.https.onRequest((req, res) => {
const foo = req.body.foo;
if (!foo) return res.status(400).send({
message: 'foo is a required parameter.'
});
res.send({
message: 'success'
});
});
This document would be helpful for you.

Resources