I try to learn from an example to use express together with handlebars on firebase.
For the express way, we can send the "app" instance directly to the "functions.https.onRequest" like...
const app = express();
...
app.get('/', (req, res) => {
...
});
exports.app = functions.https.onRequest(app);
See live functions
As my understanding it's working because "express" act like http-node, so it can respond "http plain".
Comparing to hapi, here is hello-world
const Hapi = require('hapi');
const server = new Hapi.Server();
server.connection({
host: 'localhost',
port: 8000
});
server.route({
method: 'GET',
path:'/hello',
handler: function (request, reply) {
return reply('hello world');
}
});
server.start((err) => {
console.log('Server running at:', server.info.uri);
});
From the hapi example, is it possible to use hapi on firebase cloud function?
Can I use hapi without starting a server like express?
This code was straight forward as i mixed the express API that used by Firebase with hapijs API, thanks to the blog given by mister #dkolba
You can ivoke the url hapijs handler by going to
http://localhost:5000/your-app-name/some-location/v1/hi
example: http://localhost:5000/helloworld/us-central1/v1/hi
const Hapi = require('hapi');
const server = new Hapi.Server();
const functions = require('firebase-functions');
server.connection();
const options = {
ops: {
interval: 1000
},
reporters: {
myConsoleReporter: [{
module: 'good-squeeze',
name: 'Squeeze',
args: [{ log: '*', response: '*' }]
}, {
module: 'good-console'
}, 'stdout']
}
};
server.route({
method: 'GET',
path: '/hi',
handler: function (request, reply) {
reply({data:'helloworld'});
}
});
server.register({
register: require('good'),
options,
}, (err) => {
if (err) {
return console.error(err);
}
});
// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions
exports.v1 = functions.https.onRequest((event, resp) => {
const options = {
method: event.httpMethod,
url: event.path,
payload: event.body,
headers: event.headers,
validate: false
};
console.log(options);
server.inject(options, function (res) {
const response = {
statusCode: res.statusCode,
body: res.result
};
resp.status(res.statusCode).send(res.result);
});
//resp.send("Hellworld");
});
Have a look at the inject method (last code example): http://www.carbonatethis.com/hosting-a-serverless-hapi-js-api-with-aws-lambda/
However, I don't think that this is feasible, because you would still need to hold onto the response object of the express app instance Google Cloud Functions provide to http triggered functions, as only send(), redirect() or end() will work to respond to the incoming request and not hapi's methods (see https://firebase.google.com/docs/functions/http-events).
A few changes are required in order to get compatible with hapijs 18.x.x
'use strict';
const Hapi = require('#hapi/hapi')
, functions = require('firebase-functions');
const server = Hapi.server({
routes: {cors: true},
});
server.register([
{
plugin: require('./src/plugins/tools'),
routes: {
prefix: '/tools'
}
},
]);
server.route({
method: 'GET',
path: '/',
handler: (request, h) => {
return 'It worlks!';
}
});
exports.v1 = functions.https.onRequest((event, resp) => {
//resp: express.Response
const options = {
method: event.httpMethod,
headers: event.headers,
url: event.path,
payload: event.body
};
return server
.inject(options)
.then(response => {
delete response.headers['content-encoding']
delete response.headers['transfer-encoding']
response.headers['x-powered-by'] = 'hapijs'
resp.set(response.headers);
return resp.status(response.statusCode).send(response.result);
})
.catch(error => resp.status(500).send(error.message || "unknown error"));
});
Related
So, let's say i have /api/cat/fact.js directory.
I wanna to get JSON Data from catfact.ninja
The thing is, i can't use require() or request() package, because if i used require, it would saya Couldnt Found Module..., and if i used request one, instead of returning the JSON Data that you beable to sees in catfact.ninja, it return JSON about the api, like hostname, port, which is i don't need
/API/api/cat/fact.js:
const express = require('express');
const app = express.Router();
const request = require('request')
app.use('', (req, res) => {
const src = 'https://catfact.ninja/fact';
const facts = request({
uri: src,
hostname: 'catfact.ninja',
port: 443,
path: '/fact',
method: 'POST',
json: 'fact'
}, (error, response, body) => {
if (error) console.log(error)
console.log(body, '\n\n' + response.fact)
})
console.log(facts);
return res.jsonp(facts)
})
module.exports = app;
You are returning JSON in the wrong place. It should be returned inside of the callback function.
Here's the solution:
const express = require('express');
const request = require('request-promise')
const app = express();
app.use('', async (req, res) => {
const src = 'https://catfact.ninja/fact';
try {
const response = await request({
uri: src,
port: 443,
method: 'GET',
json: true
})
return res.jsonp(response)
} catch (err) {
return res.jsonp(err)
}
})
function startServer() {
const port = 3000
app.listen(port, () => {
console.info('Server is up on port ' + port)
})
app.on('error', (err) => {
console.error(err)
process.exit(1)
})
}
startServer()
TIP: I suggest using request-promise npm package instead of request package as it provides async-await approach, which is cleaner. Else, you can continue using callback function as second request() function parameter.
I try to post json to Line' messaging api with koa.js and request-promise but got error as attached image :
I'm using heroku with koa.js to connect with Line messaging api.
Here is my code :
const Koa = require('koa');
const Router = require('koa-router');
const logger = require('koa-logger');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
const port = process.env.PORT || 4000;
app.use(logger());
app.use(bodyParser());
app.on('error', (err, ctx) => {
console.log('server error', err, ctx)
});
app.use(router.routes());
app.use(router.allowedMethods());
router
.get('/', (ctx, next) => {
console.log(ctx);
ctx.body = ctx;
})
.post('/webhook', async (ctx, next) => {
var reply_Token = ctx.request.body.events[0].replyToken;
console.log('token = ' , ctx.request.body.events[0].replyToken);
var rp_body = JSON.stringify({
replyToken: reply_Token,
messages: [{
type: 'text',
text: 'Hello'
},
{
type: 'text',
text: 'How are you?'
}]
});
var options = {
method: 'POST',
url: 'https://api.line.me/v2/bot/message/reply',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer {xxxxxxxx}'
},
json: true,
body: rp_body
};
var rp = require('request-promise');
rp(options)
.then(function (parsedBody){
console.log('rq success');
})
.catch(function (err) {
console.log('server error', err, ctx);
});
});
app.listen(port);
module.exports = { app }
After try to solving with changing variable but seem doesn't work at all. This is what I try to adapt from using Node.Js to Koa.js.
Solving the problems!, thanks to #num8er for pointing to it.
As the body entity has 'json : true' so the body is already stringify by this. There's no need to do stringify before.
So removing it like :
var rp_body = JSON.stringify({
replyToken: reply_Token,
messages: [{
to
var rp_body = ({
replyToken: reply_Token,
messages: [{
However after pull off stringify from body you might encounter 'Invalid Token' if process api.line.me verification.
It's what it should be, because api.line.me will throw zeros as reply token for verification and Koa.js look at it like an error.
So checking for if token is zeros then send status 200 to complete the verification, otherwise do the POST METHOD if token is not zeros.
if(reply_Token === '00000000000000000000000000000000') {
ctx.status = 200;
} else {
//POST METHOD
}
I'm trying to setup CSRF tokens so that I can do a number of checks before issueing a token to the client to use in future requests.
Taking the guidance from the csurf documentation, I've setup my express route with the following:
const express = require('express');
const router = express.Router({mergeParams: true});
const csurf = require('csurf');
const bodyParser = require('body-parser');
const parseForm = bodyParser.urlencoded({ extended: false });
const ErrorClass = require('../classes/ErrorClass');
const csrfMiddleware = csurf({
cookie: true
});
router.get('/getCsrfToken', csrfMiddleware, async (req, res) => {
try {
// code for origin checks removed for example
return res.json({'csrfToken': req.csrfToken()});
} catch (error) {
console.log(error);
return await ErrorClass.handleAsyncError(req, res, error);
}
});
router.post('/', [csrfMiddleware, parseForm], async (req, res) => {
try {
// this returns err.code === 'EBADCSRFTOKEN' when sending in React.js but not Postman
} catch (error) {
console.log(error);
return await ErrorClass.handleAsyncError(req, res, error);
}
});
For context, the React.js code is as follows, makePostRequest 100% sends the _csrf token back to express in req.body._csrf
try {
const { data } = await makePostRequest(
CONTACT,
{
email: values.email_address,
name: values.full_name,
message: values.message,
_csrf: csrfToken,
},
{ websiteId }
);
} catch (error) {
handleError(error);
actions.setSubmitting(false);
}
Postman endpoint seems to be sending the same data, after loading the /getCsrfToken endpoint and I manually update the _csrf token.
Is there something I'm not doing correctly? I think it may be to do with Node.js's cookie system.
I think your problem is likely to be related to CORS (your dev tools will probably have sent a warning?).
Here's the simplest working back-end and front-end I could make, based on the documentation:
In Back-End (NodeJS with Express) Server:
In app.js:
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')
const cors = require('cors');
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })
var app = express()
const corsOptions = {
origin: "http://localhost:3000",
credentials: true,
}
app.use(cors(corsOptions));
app.use(cookieParser())
app.get('/form', csrfProtection, function (req, res) {
res.json({ csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function (req, res) {
res.send('data is being processed')
})
module.exports = app;
(make sure you update the corsOptions origin property to whatever your localhost is in React.
In Index.js:
const app = require('./app')
app.set('port', 5000);
app.listen(app.get('port'), () => {
console.log('App running on port', app.get('port'));
});
In React:
Create file "TestCsurf.js" and populate with this code:
import React from 'react'
export default function TestCsurf() {
let domainUrl = `http://localhost:5000`
const [csrfTokenState, setCsrfTokenState] = React.useState('')
const [haveWeReceivedPostResponseState, setHaveWeReceivedPostResponseState] = React.useState("Not yet. No data has been processed.")
async function getCallToForm() {
const url = `/form`;
let fetchGetResponse = await fetch(`${domainUrl}${url}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"xsrf-token": localStorage.getItem('xsrf-token'),
},
credentials: "include",
mode: 'cors'
})
let parsedResponse = await fetchGetResponse.json();
setCsrfTokenState(parsedResponse.csrfToken)
}
React.useEffect(() => {
getCallToForm()
}, [])
async function testCsurfClicked() {
const url = `/process`
let fetchPostResponse = await fetch(`${domainUrl}${url}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"xsrf-token": csrfTokenState,
},
credentials: "include",
mode: 'cors',
})
let parsedResponse = await fetchPostResponse.text()
setHaveWeReceivedPostResponseState(parsedResponse)
}
return (
<div>
<button onClick={testCsurfClicked}>Test Csurf Post Call</button>
<p>csrfTokenState is: {csrfTokenState}</p>
<p>Have we succesfully navigates csurf with token?: {JSON.stringify(haveWeReceivedPostResponseState)}</p>
</div>
)
}
Import this into your app.js
import CsurfTutorial from './CsurfTutorial';
function App() {
return (
<CsurfTutorial></CsurfTutorial>
);
}
export default App;
That's the simplest solution I can make based on the CSURF documentations example. It's taken me several days to figure this out. I wish they'd give us a bit more direction!
I made a tutorial video in case it's of any help to anyone: https://youtu.be/N5U7KtxvVto
I'm following this tutorial to implement jwt authentication in hapijs v17.2.
I did everything according to the tutorial, but the following error is driving me crazy, even debugging didn't make any change.
error
Debug: internal, implementation, error
TypeError: cb is not a function
at Object.secretProvider [as key] (C:\Users\user\WebstormProjects\hapi-blog\node_modules\jwks-rsa\lib\integrations\hapi.js:30:14)
at Object.authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi-auth-jwt2\lib\index.js:123:87)
at module.exports.internals.Manager.execute (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\toolkit.js:35:106)
at module.exports.internals.Auth._authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\auth.js:242:58)
at authenticate (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\auth.js:217:21)
at module.exports.internals.Request._lifecycle (C:\Users\user\WebstormProjects\hapi-blog\node_modules\hapi\lib\request.js:261:62)
at <anonymous>
app.js
const hapi = require('hapi');
const mongoose = require('./db');
const hapi_auth_jwt = require('hapi-auth-jwt2');
const jwksa_rsa = require('jwks-rsa');
const dog_controller = require('./controllers/dog');
const server = new hapi.Server({
host: 'localhost',
port: 4200
});
const validate_user = (decoded, request, callback) => {
console.log('Decoded', decoded);
if (decoded && decoded.sub) {
return callback(null, true, {});
}
return callback(null, true, {});
};
const register_routes = () => {
server.route({
method: 'GET',
path: '/dogs',
options: {
handler: dog_controller.list,
auth: false
}
});
// Test
server.route({
method: 'POST',
path: '/a',
options: {
handler: (req, h) => {
return h.response({message: req.params.a});
},
auth: false
}
});
server.route({
method: 'GET',
path: '/dogs/{id}',
options: {
handler: dog_controller.get
}
});
server.route({
method: 'POST',
path: '/dogs',
options: {
handler: dog_controller.create
}
});
server.route({
method: 'PUT',
path: '/dogs/{id}',
handler: dog_controller.update
});
server.route({
method: 'DELETE',
path: '/dogs/{id}',
handler: dog_controller.remove
});
};
const init = async () => {
await server.register(hapi_auth_jwt);
server.auth.strategy('jwt', 'jwt', {
key: jwksa_rsa.hapiJwt2Key({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
// YOUR-AUTH0-DOMAIN name e.g https://prosper.auth0.com
jwksUri: 'https://mrvar.auth0.com/.well-known/jwks.json'
}),
verifyOptions: {
audience: 'https://mrvar.auth0.com/api/v2/',
issuer: 'https://mrvar.auth0.com',
algorithm: ['RS256']
},
validate: validate_user
});
server.auth.default('jwt');
// Register routes
register_routes();
// Start server
await server.start();
return server;
};
init().then(server => {
console.log('Server running at: ', server.info.uri);
}).catch(err => {
console.log(err);
});
When I make a request to routes with auth: false, the handler works properly then I get the expected result, but requests to routes without auth return the following json :
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "An internal server error occurred"
}
More info:
node version: 8.9.4
npm version: 5.6.0
hapi version: 17.2.0
hapi-auth-jwt2: github:salzhrani/hapi-auth-jwt2#v-17
jwks-rsa: 1.2.1
mongoose: 5.0.6
nodemon: 1.15.0
Both libraries has support for hapi v.17
I also faced this issue, but surprisingly both libraries has support for hapi v.17, but all documentation is based on old versions or didn't use this combination ;)
How to use it
There are few things that has to be changed then using hapijs v.17
Verify that you're using libraries versions which support hapijs 17:
"jwks-rsa": "^1.3.0",
"hapi-auth-jwt2": "^8.0.0",
Use hapiJwt2KeyAsync instead of hapiJwt2Key.
Information about this new async method is hidden in node-jwks-rsa package documentation
validate function has now new contract
Please change your existing validate function to below type:
async (decoded: any, request: hapi.Request): {isValid: boolean, credentials: {}}
Working example (with handled passing scopes)
const validateUser = async (decoded, request) => {
if (decoded && decoded.sub) {
return decoded.scope
? {
isValid: true,
credentials: {
scope: decoded.scope.split(' ')
}
}
: { isValid: true };
}
return { isValid: false };
};
server.auth.strategy('jwt', 'jwt', {
complete: true,
key: jwksRsa.hapiJwt2KeyAsync({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: env.auth.jwksUri
}),
verifyOptions: {
audience: '/admin',
issuer: env.auth.issuer,
algorithms: ['RS256']
},
validate: validateUser
});
The validate function changed in hapi#17 to not have a callback function. Based on your example it should now look something like this:
const validate = async (decoded, request) => {
if (decoded && decoded.sub) {
return { isValid: true };
}
return { isValid: false };
};
Part of the returned object can also include credentials which would represent the user that is authenticated and you can also do a scope as part of the credentials.
Then if you want to you can access the credentials as part of the request object like request.auth.credentials
With express.js i can do this
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);
var port = 9200;
var path = require('path');
var options = {
index: "html/index.html"
};
app.use('/', express.static('res', options));
server.listen(port);
console.log("Listening on port " + port);
How do i achieve the same thing using Hapi.js?
I have tried some things also with inertjs but I can't seem to find the correct way. Does anyone have experience with it?
Found my way to this implementation, but im getting TypeErrors:
TypeError: Cannot read property 'register' of undefined
server.register(require('inert'), (err) => {
if (err) {
throw err;
}
server.route({
method: 'GET',
path: '/index',
handler: function (request, reply) {
reply.file('C:/blabla/html/index.html');
}
});
});
Straight from the doc
In detail, you just have to call reply.file()
This will not work with hapi 17 because of the major change in the request system
server.route({
method: 'GET',
path: '/picture.jpg',
handler: function (request, reply) {
reply.file('/path/to/picture.jpg');
}
});
Fixed by using Hapi#16.x.x and Inert#4.x.x and using Ernest Jones answer.
For anyone dealing with this problem in the future here is my complete change:
const Hapi = require('hapi');
const Inert = require('inert');
const server = new Hapi.Server();
server.connection({port: 9200});
server.register(Inert, (err) => {
if (err) {
throw err;
}
server.route({
method: 'GET',
path: '/index',
handler: function (request, reply) {
reply.file('/res/html/index.html');
}
});
server.route({
path: "/res/{path*}",
method: "GET",
handler: {
directory: {
path: "./res",
listing: false,
index: false
}
}});
});
server.start();