Firebase Functions HTTPS 403 Forbidden - node.js

I built a Firebase HTTP Event function with Node and Express. The function is working, but when I invoke the function on the client side I get 403 Forbidden. The first time I invoked the function I was asked to sign in with a Google account. I signed in with the same account I use for Firebase, but when I invoked the function I got:
Screenshot of 403 error
I looked at the use roles on Google cloud platform and the permission to invoke the function is set to allUsers. I signed out and back in again in the Firebase CLI.
Here is the index.js in the functions folder:
const functions = require('firebase-functions');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const port = process.env.port || 5600
const nodemailer = require('nodemailer');
app.use(express.static('Public'));
app.use(bodyParser.urlencoded({ extended: true }));
const urlencodedParser = bodyParser.urlencoded({extended: true});
app.post("/api/user", urlencodedParser, (req, res) => {
res.sendFile('../Public/bedankt.html', {root: __dirname})
const persGegevens = req.body
const string = JSON.stringify(persGegevens, (key, value) => {
if (typeof value === "string"){
return value.toUpperCase();
} else {
return value
}
}, 1);
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'gietvloermakers#gmail.com',
pass: 'Gietvloermakers2020!'
}
});
var mailOptions = {
from: 'gietvloermakers#gmail.com',
to: 'gvbeusekom84#hotmail.com',
subject: 'Nieuwe bestelling op Gietvloermakers',
html: string
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
} else {
console.log('Email sent: ' + info.response);
}
});
});
exports.app1 = functions.https.onRequest(app);
app.listen(port);
console.log(port);
Here is the html:
<form id="controlleer-form" action="/api/user" method="post" enctype="application/x-www-form-urlencoded">
<div class="controleer-div">
<h2>Uw bestelling</h2>
<p>Aantal m2</p>
<input class="controle-input" type="text" name="aantalM2" id="aantalM2" readonly>
<p>Kleur</p>
<input class="controle-input" type="text" name="kleur" id="kleur" readonly>
<p>Assistentie</p>
<input class="controle-input" type="text" name="assistentie" id="assistentie" readonly>
<p>Gereedschappen</p>
<input class="controle-input" type="text" name="gereedschappen" id="gereedschappen" readonly>
<p>Totale prijs</p>
<input class="controle-input" type="text" name="totale-prijs" id="totale-prijs" readonly>
<p id="andere-kleur">Bestelling aanpassen</p>
</div>
<div class="controleer-div">
<h2>Uw gegevens</h2>
<p>Voornaam</p>
<input type="text" name="voornaam" placeholder="Voornaam">
<p>Achternaam</p>
<input type="text" name="Achternaam" placeholder="Achternaam">
<p>Straatnaam en huisnummer</p>
<input type="text" name="Achternaam" placeholder="Straatnaam en huisnummer">
<p>Postcode</p>
<input type="text" name="Achternaam" placeholder="Postcode">
<p>Telefoonnummer</p>
<input type="tel" name="telefoonnummer" placeholder="Telefoonnummer">
<p>Emailadres</p>
<input type="email" name="email" placeholder="Emailadres"><br>
<input id="verzenden" type="submit">
</div>
</form>
Here is the firebase.json:
{
"hosting": {
"public": "Public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [{
"source": "**",
"function": "app1"
}]
}
}
I tried but I exhausted all possible solutions I've found online so far.

I encountered this recently. It turns out that as of January 15, 2020 new functions require authentication by default.
See the docs here for details.
The solution was to manually add the Cloud Functions Invoker permission to the allUsers user in the Cloud Functions page in the Google Cloud Console.

If you are getting 403 forbidden error like below
Error: Forbidden Your client does not have permission to get URL
/api/test from this server.
Please follow below steps to grant access to all users. Basically this is to allow unauthenticated clients to access your api endpoint.
Go to https://console.cloud.google.com/functions/list
Select the function to which you want to give public access
Click on PERMISSIONS
Click on ADD MEMBER
Type allUsers
Select role Cloud Functions -> Cloud Functions Invoker
Save
That's it, now test your api.

This has to do with permission access to your cloud functions http requests and cloud function events, you need to edit your cloud function IAM permission.
https://cloud.google.com/functions/docs/securing/managing-access-iam#allowing_unauthenticated_function_invocation

Had the same problem (was asked to login with my Google Account, then denied access). It turned out that functions do currently not work outside the default region. In my case, I had to make a change here:
exports.app = functions
.region('europe-west6') // does not work, delete this line
.https.onRequest(app);

Your code exports the express application as the Cloud Function app1 on this line:
exports.app1 = functions.https.onRequest(app);
In your screenshot, you have tried to access the non-existent app Cloud Function instead resulting in the 403 Forbidden response.
This means the correct URL to call from your client is
http://us-central1-gietvloermakers.cloudfunctions.net/app1/api/user
^^^^
(or you could change the name of the export to app)
Having a closer look at your source code, you should also remove the following lines. If you wanted to test your code you would instead use firebase serve.
const port = process.env.port || 5600
/* ... */
app.listen(port);
On the following lines, you also inject the body parser twice.
app.use(bodyParser.urlencoded({ extended: true })); // use this
const urlencodedParser = bodyParser.urlencoded({extended: true}); // or this, not both
app.post("/api/user", urlencodedParser, ...
In your code, you also have:
app.post("/api/user", urlencodedParser, (req, res) => {
res.sendFile('../Public/bedankt.html', {root: __dirname})
/* do some other stuff */
})
This is invalid for a Cloud Function, because as soon as the Cloud Function handler (your code) calls end(), redirect() or send(), the Cloud Function is allowed to be terminated at any time which means that your email may never be sent. To fix this you need to send the file last.
app.post("/api/user", urlencodedParser, (req, res) => {
/* do some other stuff */
res.sendFile('../Public/bedankt.html', {root: __dirname})
});
My last observation, is that the error may be caused by the folder Public not existing on the server. Based on your sendFile call, you are expecting that the folder "Public" is available to your deployed function but as it is not inside the functions folder, it will not be deployed with your code.
res.sendFile('../Public/bedankt.html', {root: __dirname})
As this file would also be accessible at your-domain.com/bedankt.html, we'll redirect to it. If you wanted to send the HTML content of this file instead, move it inside your deployed functions directory.
res.redirect('/bedankt.html')
Because you appear to be trying to use your express function behind Firebase hosting, we can trim your index.js file to the following:
const functions = require('firebase-functions');
const express = require('express');
const bodyParser = require('body-parser');
const nodemailer = require('nodemailer');
const apiApp = express();
apiApp.use(bodyParser.urlencoded({ extended: true }));
apiApp.post("/api/user", (req, res) => {
const persGegevens = req.body
const string = JSON.stringify(persGegevens, (key, value) => {
if (typeof value === "string"){
return value.toUpperCase();
} else {
return value
}
}, 1);
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'gietvloermakers#gmail.com',
pass: 'Gietvloermakers2020!'
}
});
var mailOptions = {
from: 'gietvloermakers#gmail.com',
to: 'gvbeusekom84#hotmail.com',
subject: 'Nieuwe bestelling op Gietvloermakers',
html: string
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
res.redirect('/bedankt.html?success=0');
} else {
console.log('Email sent: ' + info.response);
res.redirect('/bedankt.html?success=1');
}
});
});
// note rename to api
exports.api = functions.https.onRequest(apiApp);
which requires updating your firebase.json file to:
{
"hosting": {
"public": "Public",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [{
"source": "/api/**",
"function": "api"
}]
}
}
This configuration will first attempt to find a matching file in your Public directory. If it can't find a match, it will check if the requested path starts with /api and if so, launch your Cloud Function. If it still can't find a match, it will show your 404 page (or the built in one if it doesn't exist).

Related

How to make external GET request using Nuxt.js Server Middleware

I am working with a Nuxt.js v2.15.8 project and I am attempting to use the server middleware feature that Nuxt offers for A custom API endpoint. https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/#custom-api-endpoint
What I am trying to accomplish:
Use Nuxt server middleware to make a GET request to a 3rd party api to retrieve data. When I try to set this up and make the request to the endpoint in Postman, I get an error
<!doctype html>
<html data-n-head-ssr lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22en%22%7D%7D">
<head>
<title>This page could not be found</title> etc....
How do I use the Nuxt server middleware to make api calls to external api's?
Nuxt.config.js
serverMiddleware: [
{
path: '/api/server-middleware',
handler: '~/api/getData.js',
},
],
~/api/getData.js
const bodyParser = require('body-parser');
const app = require('express')();
app.use(bodyParser.json());
app.all('https://jsonplaceholder.typicode.com/todos/1', (req, res) => {
res.json({ data: res.data });
});
module.exports = app;
In Postman I try to make a GET request to http://localhost:3000/api/server-middleware after running npm run dev and my Nuxt project is running.
Am I misunderstanding how this is supposed to work? Is the Server Middleware for internal api calls only?
Applying the least possible amount of changes to your shared code gives us the following
getData.js
import axios from 'axios'
const app = require('express')()
app.all('/jsonplaceholder/:id', async (req, res) => {
const { data } = await axios(
`https://jsonplaceholder.typicode.com/todos/${req.params.id}`
)
res.json({ ...data })
})
module.exports = app
/pages/index.vue
<template>
<div>
<input id="name" v-model="todoId" type="text" name="name" />
<button #click="callNuxtApi">try local Nuxt API</button>
<div>
Response from the backend:
<pre>{{ response }}</pre>
</div>
</div>
</template>
<script>
export default {
name: 'JsonPlaceholderPage',
data() {
return {
todoId: 1,
response: {},
}
},
methods: {
async callNuxtApi() {
const response = await this.$axios.$get(`/api/server-middleware/jsonplaceholder/${this.todoId}`)
console.log('response', response)
this.response = response
},
},
}
</script>
As you can see, /jsonplaceholder/:id is something more reasonable considering that it will be prefixed by /api/server-middleware/ already.
Having https:// inside of a path is not really nice to the browser overall.
PS: you need to install axios and express for it to work. #nuxtjs/axios will not work here.
This answer joins my other one here: https://stackoverflow.com/a/72102209/8816585

React is having trouble rendering elements when I serve the React from the same origin as express

I'm serving a React build from the same port (port 3000) that my express files receive requests on. Below is my code for doing so.
app.use(express.static(path.join(__dirname, "client", "build")))
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, "client", "build", "index.html"))
})
In my react app, I make an API call to the server and the API's response updates the state. I know (from testing) that I'm getting the correct response from the server, and I know that the state is properly updated with the response. But when I try to render <p>{this.state.data}</p>, the entire app breaks and nothing is rendered.
Anyone have any ideas about why this is happening? I tried serving the React app from its folder on a different port and it still doesn't work. Posting all relevant code below for reference.
React Code
import React from 'react';
import './App.css';
class Form extends React.Component{
constructor(props){
super(props);
}
onTrigger = (event) => {
this.props.callBack();
}
render(){
return(
<div>
<form action="/photoupload" method="POST" className="" encType="multipart/form-data">
<label for="species">Species Name:
<input type="text" name="species" id="species"/>
</label>
<label for="description">Description:
<textarea name="description" id="description" placeholder="Enter a description of your shroom..."></textarea>
</label>
<label for="photo">Photo:
<input type="file" name="mushroom" id="mushroom" accept="image/*"/>
</label>
<input onClick={this.onTrigger} type="submit"/>
</form>
</div>
)
}
}
class App extends React.Component {
constructor(props){
super(props);
this.state = {
data: ""
};
}
componentDidMount(){
fetch('/api', {method: "GET"})
.then(response => response.json())
.then(json => this.setState({
data: json
}))
console.log(this.state)
}
handleClick = () => {
console.log("Hello!")
}
render(){
return (
<div>
<Form callBack={this.handleClick} />
<p>{this.state.data}</p>
</div>
)
}
}
export default App;
Server Code
const express = require('express')
const app = express();
const bodyParser = require('body-parser')
const multer = require('multer')
const upload = multer({dest: 'uploads/'})
const fs = require('fs')
const Mushroom = require('./db')
const path = require('path')
const cors = require('cors')
app.use(bodyParser.urlencoded({extended: false}));
app.use(express.static(path.join(__dirname, "client", "build")))
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, "client", "build", "index.html"))
})
app.get('/api', (req, res) => {
const mushroom = Mushroom.find({}, (err, docs) => {
res.send(docs)
});
})
app.post('/photoupload', upload.single('mushroom'), function (req, res) {
var mushroom = new Mushroom();
mushroom.species = req.body.species;
mushroom.description = req.body.description;
mushroom.path = req.file.path;
mushroom.save()
res.redirect("/")
})
app.listen(3000, () => {
console.log("listening on 3000!")
})
EDIT
I'm just going to copy and paste the errors below:
App.js:51 Objectdata: "{_id: 132454}"__proto__: Object
:3000/manifest.json:1 Failed to load resource: the server responded with a status of 404 (Not Found)
:3000/manifest.json:1 Manifest: Line: 1, column: 1, Syntax error.
:3000/favicon.ico:1 Failed to load resource: the server responded with a status of 404 (Not Found)
react-dom.production.min.js:216 Error: Minified React error #31; visit https://reactjs.org/docs/error-decoder.html?invariant=31&args[]=object%20with%20keys%20%7B_id%2C%20age%2C%20species%2C%20description%2C%20path%2C%20__v%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at ka (react-dom.production.min.js:140)
at d (react-dom.production.min.js:144)
at m (react-dom.production.min.js:146)
at react-dom.production.min.js:150
at Do (react-dom.production.min.js:176)
at Hu (react-dom.production.min.js:271)
at Pi (react-dom.production.min.js:250)
at xi (react-dom.production.min.js:250)
at _i (react-dom.production.min.js:250)
at vi (react-dom.production.min.js:243)
uu # react-dom.production.min.js:216
react-dom.production.min.js:140 Uncaught (in promise) Error: Minified React error #31; visit https://reactjs.org/docs/error-decoder.html?invariant=31&args[]=object%20with%20keys%20%7B_id%2C%20age%2C%20species%2C%20description%2C%20path%2C%20__v%7D for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
at ka (react-dom.production.min.js:140)
at d (react-dom.production.min.js:144)
at m (react-dom.production.min.js:146)
at react-dom.production.min.js:150
at Do (react-dom.production.min.js:176)
at Hu (react-dom.production.min.js:271)
at Pi (react-dom.production.min.js:250)
at xi (react-dom.production.min.js:250)
at _i (react-dom.production.min.js:250)
at vi (react-dom.production.min.js:243)
As per your error, it seems the {this.state.data} is an object and have only 1 property called id so you might change it to {this.state.data.id} instead
If you go to the Minified error page from your console log, you can see that the error is from trying to render an object, which won't work with <p>.
It depends on what you want to render the data as, to see the data as a string, change your line to <p>{JSON.stringify(this.state.data)</p> and it will render the object as a string.

How to receive a file from frontend using post method and upload it to google cloud?

I am trying to upload a file to google storage bucket. The file will be sent to the nodejs backend route using post method and form-data. I have read google storage documents regarding uploading the files.
I am not getting the idea about how should i receive the file and assign it to filename variable so that it can be uploaded to google storage bucket. Something just like this.
const {Storage} = require('#google-cloud/storage');
// Creates a client
const storage = new Storage();
test: async (req, res, next) => {
const filename = req.body.file
// const bucketName = 'Name of a bucket, e.g. my-bucket';
// const filename = 'Local file to upload, e.g./local/path/to/file.txt';
await storage.bucket(bucketName).upload(filename, {
gzip: true,
metadata: {
cacheControl: 'public, max-age=31536000',
},
});
console.log(`${filename} uploaded to ${bucketName}.`);
}
}
I have created the code below from the samples of node-formidable library to handle form data and the snippets for nodejs-Storage library. Indeed I tested it and works pretty well, tough you may want to customize filenames and the like.
const express = require('express');
const formidable = require('formidable');
const {Storage} = require('#google-cloud/storage');
const bucket = 'yourBucketName';
async function uploadFile(bucket, filename) {
const storage = new Storage();
const params = {metadata: {cacheControl: 'public, max-age=31536000'}};
await storage.bucket(bucket).upload(filename, params);
console.log(`${filename} uploaded to ${bucket}.`);
}
const app = express();
app.get('/', (req, res) => {
res.send(`
<h2>With <code>"express"</code> npm package</h2>
<form action="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div>
<input type="submit" value="Upload" />
</form>
`);
});
app.post('/api/upload', (req, res, next) => {
const form = formidable();
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
let imgPath = files.someExpressFiles.path;
uploadFile(bucket, imgPath).catch(console.error);
res.json({ fields, files });
});
});
app.listen(8080, () => {
console.log('Server listening on http://localhost:8080 ...');
});
The gg docs solution only works in development.
If you are in production, your Nodejs won't aware of file path
So follow these steps may help you:
Step 1: Encode your image to base64 and send it to backend.
Step 2: From your backend, you have to decode this base64 and upload it to gg cloud storage
this link is helpful: How do I upload a base64 encoded image (string) directly to a Google Cloud Storage bucket using Node.js?

How to send an email via form input with angular and node once deployed on heroku?

I deployed an app on heroku with angular for the front-end and node(express) as the back-end. On the contact page, I have a form that will be sent via email once the send button is pressed, which then sends the info via HTTP to node for the user inputs to be passed through the node function - I'm using SendGrid addon for the email process to send the email. I tried multiple time, but I just can't get it to work and can't find a tutorial with angular. It worked before with nodemailer on the localhost, but I can't figure it out once it is deployed. Thank you a lot for your time.
node code app.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');
const sgMail = require('#sendgrid/mail');
const port = process.env.PORT || 3000;
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'client')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'client/index.html'));
});
app.post('/client-contact', (req, res) => {
const clientMsg = `
<p> Email sent from your portfolio website</p>
<h3>Contact details</h3>
<ul>
<li>Name ${req.body.name}</li>
<li>Email: ${req.body.email}</li>
</ul>
<h3>Message</h3>
<p>${req.body.message}</p>
`;
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: 'mrwanzein#outlook.com',
from: 'mrwanzein#outlook.com',
subject: 'Angular portfolio form user response',
html: clientMsg,
};
sgMail.send(msg);
});
app.listen(port, () => {
console.log(`Server is on on port ${port}`);
});
the function that register the user input and then sends it to node (and handles some validations, the interest is the last line) contact.component.ts
sendMail() {
var name = <HTMLInputElement>document.getElementById('inputName'),
email = <HTMLInputElement>document.getElementById('inputEmail'),
msg = <HTMLInputElement>document.getElementById('inputMsg');
var obj = {
name: name.value,
email: email.value,
message: msg.value
}
let validateEmail = () => {
const re = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?
^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])/;
return re.test(email.value);
}
if(!name.value || !email.value || !msg.value || !validateEmail()) {
this.bool = true;
} else {
this.bool = false;
setTimeout(() => {
window.location.reload()
}, 1500);
return this.http.post('https://mrwanzein.herokuapp.com/client-contact', obj).subscribe();
}
}
You need to configure an SMTP server for your application.
The easiest way is with a heroku addin such as mailgun
I fixed it, basically the code is fine, I had to remove the res.send() around sgMail.send(msg) and I forgot to ng build(angular) to update the HTTP address when I changed it. I was pushing changes to the repo without the missing updated code, oh dear... :P

Nodemailer /send gives 404 error

I'm trying to use nodemailer to send a message from a standard website form to an email address. I have followed this tutorial and everything works on the local server. But when I upload the documentation to my hosting provider I get the following error:
POST http://mydomain.rs/send 404 (Not Found)
Not Found
The requested URL /send was not found on this server.
Additionally, a 404 Not Found error was encountered while trying to use an
ErrorDocument to handle the request.
I have three files relevant to the nodemailer, those are:
app.js
var http = require('http');
var express = require('express');
var nodemailer = require("nodemailer");
var bodyParser = require('body-parser');
var path = require('path');
var app = express();
var port = Number(process.env.PORT || 5000);
app.use(bodyParser.json()); // to support JSON-encoded bodies
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(express.static(__dirname + '/'));
// Home page
app.get('/', function (req, res) {
res.sendFile('index.html');
//console.log
console.log('NodeMailer reading console log...' + req.url);
});
// sending mail function
app.post('/send', function (req, res) {
if (req.body.email == "" || req.body.subject == "") {
res.send("Error: Email & Subject should not blank");
return false;
}
// Sending Emails with SMTP, Configuring SMTP settings
var smtpTransport = nodemailer.createTransport("SMTP", {
host: "mail.mydomain.rs", // hostname
secureConnection: true, // use SSL
port: 465, // port for secure SMTP
auth: {
user: 'messages#mydomain.rs',
pass: 'password'
},
proxy: 'http://mydomain.rs:' + port
});
var mailOptions = {
from: "Node Emailer ✔ <messages#mydomain.rs>", // sender address
to: req.body.emailto, // list of receivers
subject: req.body.subject + " ✔", // Subject line
//text: "Hello world ✔", // plaintext body
html: "<b>" + req.body.description + "</b>" // html body
}
smtpTransport.sendMail(mailOptions, function (error, response) {
if (error) {
res.send("Email could not sent due to error: " + error);
} else {
res.send("Email has been sent successfully");
}
});
});
// Starting server
var server = http.createServer(app).listen(port, function () {
console.log("Server is Running on 127.0.0.1:" + port);
});
the part of the index page relevant to the form:
index.html
<form role="form" id="emailForm" method="post">
<div class="form_content">
<h2>KONTAKTIRAJTE NAS</h2>
<p id="msg"></p>
<input type="text" name="ime" placeholder="IME" required="required" />
<input type="tel" name="telefon" placeholder="TELEFON" />
<p class="mini_text">
Otvoreni od 8h - 17h
<br> Zatvoreni vikendom osim za posebne usluge
</p>
<input type="email" name="emailto" value="myactualemail#gmail.com" style="visibility: hidden; position: absolute;">
<input type="email" name="email" placeholder="EMAIL" required="required" />
<textarea name="description" placeholder="PORUKA" required="required"></textarea>
<button id="send" type="button">POŠALJI</button>
</div>
</form>
and the package.json
{
"name": "speednodemailers",
"version": "0.0.1",
"description": "NodeJs email form to send email using nodejs",
"dependencies": {
"body-parser": "^1.13.3",
"express": "^3.21.2",
"node-mailer": "^0.1.1",
"nodemailer": "^0.7.1"
}
}
So as I mentioned everything works perfectly on localhost, the problem occurs when I upload the project to my hosting provider.
Is there a step to the upload that I am missing?
Or a setting that I overlooked?
Any kind of help will be greatly appreciated as I am somewhat of a beginner at back-end stuff.
Thanks!

Resources