Trying to run apollo-server in hapi. I have working cookie authorization in hapi, but I can't control queries to graphql. validateFunc is not called on graphql queries.
validateFunc is called after login with http://localhost:8080/login
But queries to http://localhost:8080/graphql do not call validateFunc.
If i'll set auth mode of graphql to required, i'll receive not authorizaed error, but validateFunc still not called.
const Hapi = require("#hapi/hapi"); //v18.3.2
const { ApolloServer, gql } = require("apollo-server-hapi"); //v2.8.1
const typeDefs = gql`
type Query {
_: String
}
type Mutation {
_: String
}
`;
const resolvers = {
Query: {},
Mutation: {}
};
const runServer = async () => {
const server = new Hapi.server({ port: 8080 });
await server.register(require("#hapi/cookie"));
server.auth.strategy("session", "cookie", {
cookie: {
name: "test-project",
password:
"long-long-password-long-long-password-long-long-password-long-long-password",
isSecure: false
},
validateFunc: async (request, session) => {
console.log("Why it is not called on graphql queries???");
return { valid: true, credentials: "account" };
}
});
server.auth.default("session");
//GRAPH QL!
const graphqlServer = new ApolloServer({
typeDefs,
resolvers,
context: async function(q) {
console.log("Graphql context was called!");
return q;
},
introspection: false,
playground: true,
route: {
options: {
auth: {
mode: "try"
}
}
}
});
server.route([
{
method: "GET",
path: "/login",
handler: function(request, h) {
request.cookieAuth.set({ id: "key" });
return "LOGIN!";
},
options: {
auth: {
mode: "try"
}
}
},
{
method: "GET",
path: "/test",
handler: function(request, h) {
console.log("TEST");
return "OK";
},
options: {
auth: {
mode: "try"
}
}
}
]);
await graphqlServer.applyMiddleware({
app: server,
route: {
auth: { mode: "try" }
}
});
await graphqlServer.installSubscriptionHandlers(server.listener);
await server.start().then(() => {
console.log(`🚀 Server ready at ${server.info.uri}/graphql `);
});
};
runServer();
Expecting graphql queries execute hapi's validateFunc()
Related
I have a function that I am trying to test. I want to test that lambda.invoke is called with the correct arguments, however my test keeps saying that .invoke is never called. I think I am spying incorrectly. How can I spy on the .invoke and the .promise function?
Here is the function:
/* Amplify Params - DO NOT EDIT
ENV
FUNCTION_EMAIL_NAME
STRIPE_ENDPOINT_SECRET
REGION
Amplify Params - DO NOT EDIT */
const stripe = require('stripe');
const AWS = require('aws-sdk');
/**
* #type {import('#types/aws-lambda').APIGatewayProxyHandler}
*/
exports.handler = async (event) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
const lambda = new AWS.Lambda({ region: process.env.REGION });
const sig = event.headers['Stripe-Signature'];
let stripeEvent;
try {
stripeEvent = stripe.webhooks.constructEvent(
event.body,
sig,
process.env.STRIPE_ENDPOINT_SECRET
);
} catch (err) {
return {
statusCode: 400,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
},
body: JSON.stringify(`Webhook Error: ${err.message}`),
};
}
// Handle the event
switch (stripeEvent.type) {
case 'payment_intent.succeeded':
// eslint-disable-next-line no-case-declarations
const paymentIntent = stripeEvent.data.object;
// Then define and call a function to handle the stripeEvent payment_intent.succeeded
try {
const result = await lambda
.invoke({
FunctionName: process.env.FUNCTION_EMAIL_NAME,
Payload: JSON.stringify({
...event,
pathParameters: {
userID: paymentIntent.metadata.userID,
},
body: JSON.stringify({
user: {
emailAddress: paymentIntent.metadata.email,
},
}),
}),
})
.promise();
if (result.StatusCode === 200) {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
},
body: JSON.stringify('Email successfully sent!'),
};
}
} catch (err) {
console.log('Email failed!', err);
}
break;
// ... handle other stripeEvent types
default:
console.log(`Unhandled stripeEvent type ${stripeEvent.type}`);
}
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': '*',
},
body: JSON.stringify('Hello from Lambda!'),
};
};
And here is the failing test:
const AWS = require('aws-sdk');
const stripe = require('stripe');
const { handler } = require('./index');
const event = require('./event.json');
jest.mock('stripe');
jest.mock('aws-sdk');
describe('handler', () => {
const { env } = process;
beforeEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
jest.clearAllMocks();
jest.resetModules();
process.env = {
...env,
REGION: 'us-east-1',
FUNCTION_EMAIL_NAME: 'example_function_name',
STRIPE_ENDPOINT_SECRET: 'stripe_test_key',
};
});
afterEach(() => {
process.env = env;
});
it('should call the email lambda if payment intent succeeded', async () => {
const lambdaSpy = jest.spyOn(AWS, 'Lambda');
const stripeSpy = jest.spyOn(stripe.webhooks, 'constructEvent');
const test = jest.fn();
stripeSpy.mockReturnValue({
type: 'succeeded',
});
lambdaSpy.mockReturnValue({ invoke: test, promise: jest.fn() });
await handler(event);
expect(test).toHaveBeenCalled();
});
});
I trying to serve dynamic file using Hapi framework, but I got error like this => Error: Missing helper: "handlebars": Missing helper: "handlebars"
Tutorial that I watched : https://www.youtube.com/watch?v=NTrro_tLzu4&list=PLkqiWyX-_LotaQ9AuppIAXl0xyV-P5Ms-&index=6
Here's my code:
📁server.js
/* eslint-disable linebreak-style */
/* eslint-disable max-len */
'use strict';
const Hapi = require('#hapi/hapi');
const Inert = require('#hapi/inert');
const Vision = require('#hapi/vision');
const Handlebars = require('handlebars');
const path = require('path');
const routes = require('./routes');
const init = async () => {
// BUILD SERVER
// eslint-disable-next-line new-cap
const server = Hapi.Server({
host: 'localhost',
port: 1234,
routes: {
files: {
relativeTo: path.join(__dirname, 'static'),
},
},
});
// PLUGIN CONFIGURATION
// Add Hapi Plugin - Only Hapi Plugin Can Be Registered and Don't Forget
// To Install The Plugin First
// 'plugin' key is must
await server.register([
{
plugin: require('hapi-geo-locate'),
options: {
// to able track the user ip
enabledByDefault: true,
},
},
{
plugin: Inert,
},
{
plugin: Vision,
},
]);
// MAKE HANDLEBARS AVAILABLE
server.views({
engines: {
html: Handlebars,
},
path: path.join(__dirname, 'views'),
});
// ROUTE
server.route(routes);
// SERVER CONFIGURATION
await server.start();
console.log(`Server started on: ${server.info.uri}`);
};
process.on('unhandleRejection', (err) => {
console.log(err);
process.exit(1);
});
init();
📁routes.js
/* eslint-disable linebreak-style */
/* eslint-disable max-len */
const {welcomePage,
servingDynmaicPage,
loginPage,
downloadPage,
locationPage,
usersPage,
gradePage,
unavailablePage} = require('./handler');
const routes = [
{
method: 'GET',
path: '/',
handler: welcomePage,
// options: {
// files: {
// relativeTo: path.join(__dirname, 'static'),
// },
// },
},
{
method: 'GET',
path: '/dynamicPage',
handler: servingDynmaicPage,
},
{
method: 'POST',
path: '/loginPage',
handler: loginPage,
},
{
method: 'GET',
path: '/downloads',
handler: downloadPage,
},
{
method: 'GET',
path: '/locationPage',
handler: locationPage,
},
{
method: 'GET',
path: '/users/{name?}',
handler: usersPage,
},
{
method: 'GET',
path: '/School',
handler: gradePage,
},
{
method: 'GET',
path: '/{any*}',
handler: unavailablePage,
},
];
module.exports = routes;
📁handler.js
/* eslint-disable linebreak-style */
/* eslint-disable max-len */
const welcomePage = (request, h) => {
return h.file('welcome.html');
};
const servingDynmaicPage = (request, h) => {
const data = {
name: 'Adit',
};
return h.view('index', data);
};
const loginPage = (request, h) => {
if (request.payload.username === 'Adit' && request.payload.password === '123') {
return h.file('login.html');
} else {
return h.redirect('/');
}
};
const downloadPage = (request, h) => {
return h.file('welcome.html', {
mode: 'attachment',
filename: 'welcome-download.html',
});
};
const locationPage = (request, h) => {
if (request.location) {
return request.location.ip;
} else {
return '<h1>Your Location is not enabled by default!</h1>';
}
};
const usersPage = (request, h) => {
if (request.params.name) {
return `<h1>Hello! ${request.params.name}</h1>`;
} else {
return `<h1>Hello! Stranger</h1>`;
}
};
const gradePage = (request, h) => {
return `<h1>Welcome to grade ${request.query.grade}</h1>`;
};
const unavailablePage = (request, h) => {
return h.redirect('/');
};
module.exports = {welcomePage,
servingDynmaicPage,
loginPage,
downloadPage,
locationPage,
usersPage,
gradePage,
unavailablePage};
This is my server file.
In context I am not getting the request while my test is getting pass while test the required scenario.
export async function buildTestServer({
user,
headers,
roles,
}: {
user?: User;
headers?: { [key: string]: string };
roles?: Role;
}) {
const schema = await tq.buildSchema({
authChecker: AuthChecker,
validate: false,
resolvers: allResolvers(),
scalarsMap: [{ type: GraphQLScalarType, scalar: DateTimeResolver }],
});
const server = new ApolloServer({
schema,
context: async ({ req }) => {
const authHeader = headers?.authorization;
if (authHeader) {
const token = extractTokenFromAuthenticationHeader(authHeader);
try {
const user = await new UserPermissionsService(token).call();
return { req, user };
} catch {
return { req };
}
} else {
if (user) {
let capabilities: any = [];
if (roles) {
capabilities = roles.capabilities;
}
return {
req,
user: {
id: user.id,
customerId: user.customerId,
capabilities,
},
};
} else {
return { req };
}
}
},
});
return server;
}
And this is my test file from where I am sending the request to the server.
My test is getting passed but I am not getting the request headers. I want to check the the request. Can anybody help me out ?
const GET_LIST = `
query GetList($listId: String!) {
GetList(listId: $listId) {
id
}
}
`;
test('Get Lists', async () => {
const customer = await CustomerFactory.create();
const user = await UserFactory.create({ customerId: customer.id });
const list = await ListFactory.create({
customerId: customer.id,
});
const server = await buildTestServer({ user });
const result = await server.executeOperation({
query: GET_LIST,
variables: {
listId: list.id
},
});
var length = Object.keys(result.data?.GetList).length;
expect(length).toBeGreaterThan(0);
});
I have spent the night looking for solutions to this issue, it seems like a lot of people have it and the best advice is often "just switch to SPA mode", which is not an option for me.
I have JWT for authentication, using the JWTSessions gem for Rails.
On the frontend, I have Nuxt with nuxt-auth, using a custom scheme, and the following authorization middleware:
export default function ({ $auth, route, redirect }) {
const role = $auth.user && $auth.user.role
if (route.meta[0].requiredRole !== role) {
redirect('/login')
}
}
The symptom I have is as follows: if I log in and navigate around restricted pages, everything works as expected. I even have fetchOnServer: false for restricted pages, as I only need SSR for my public ones.
However, once I refresh the page or just navigate directly to a restricted URL, I get immediately redirected to the login page by the middleware. Clearly, the user that's authenticated on the client side is not being authenticated on the server side too.
I have the following relevant files.
nuxt.config.js
...
plugins: [
// ...
{ src: '~/plugins/axios' },
// ...
],
// ...
modules: [
'cookie-universal-nuxt',
'#nuxtjs/axios',
'#nuxtjs/auth'
],
// ...
axios: {
baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com/v1' : 'http://localhost:3000/v1',
credentials: true
},
auth: {
strategies: {
jwtSessions: {
_scheme: '~/plugins/auth-jwt-scheme.js',
endpoints: {
login: { url: '/signin', method: 'post', propertyName: 'csrf' },
logout: { url: '/signin', method: 'delete' },
user: { url: '/users/active', method: 'get', propertyName: false }
},
tokenRequired: true,
tokenType: false
}
},
cookie: {
options: {
maxAge: 64800,
secure: process.env.NODE_ENV === 'production'
}
}
},
auth-jwt-scheme.js
const tokenOptions = {
tokenRequired: true,
tokenType: false,
globalToken: true,
tokenName: 'X-CSRF-TOKEN'
}
export default class LocalScheme {
constructor (auth, options) {
this.$auth = auth
this.name = options._name
this.options = Object.assign({}, tokenOptions, options)
}
_setToken (token) {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
}
}
_clearToken () {
if (this.options.globalToken) {
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
this.$auth.ctx.app.$axios.setHeader('Authorization', false)
}
}
mounted () {
if (this.options.tokenRequired) {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
}
return this.$auth.fetchUserOnce()
}
async login (endpoint) {
if (!this.options.endpoints.login) {
return
}
await this._logoutLocally()
const result = await this.$auth.request(
endpoint,
this.options.endpoints.login
)
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + result
: result
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async setUserToken (tokenValue) {
await this._logoutLocally()
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + tokenValue
: tokenValue
this.$auth.setToken(this.name, token)
this._setToken(token)
}
return this.fetchUser()
}
async fetchUser (endpoint) {
if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
return
}
if (!this.options.endpoints.user) {
this.$auth.setUser({})
return
}
const user = await this.$auth.requestWith(
this.name,
endpoint,
this.options.endpoints.user
)
this.$auth.setUser(user)
}
async logout (endpoint) {
if (this.options.endpoints.logout) {
await this.$auth
.requestWith(this.name, endpoint, this.options.endpoints.logout)
.catch(() => {})
}
return this._logoutLocally()
}
async _logoutLocally () {
if (this.options.tokenRequired) {
this._clearToken()
}
return await this.$auth.reset()
}
}
axios.js
export default function (context) {
const { app, $axios, redirect } = context
$axios.onResponseError(async (error) => {
const response = error.response
const originalRequest = response.config
const access = app.$cookies.get('jwt_access')
const csrf = originalRequest.headers['X-CSRF-TOKEN']
const credentialed = (process.client && csrf) || (process.server && access)
if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
if (process.server) {
$axios.setHeader('X-CSRF-TOKEN', csrf)
$axios.setHeader('Authorization', access)
}
const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })
if (newToken.data.csrf) {
$axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
$axios.setHeader('Authorization', newToken.data.access)
if (app.$auth) {
app.$auth.setToken('jwt_access', newToken.data.csrf)
app.$auth.syncToken('jwt_access')
}
originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
originalRequest.headers.Authorization = newToken.data.access
if (process.server) {
app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
}
return $axios(originalRequest)
} else {
if (app.$auth) {
app.$auth.logout()
}
redirect(301, '/login')
}
} else {
return Promise.reject(error)
}
})
}
This solution is already heavily inspired by material available under other threads and at this point I am pretty much clueless regarding how to authenticate my users universally across Nuxt. Any help and guidance much appreciated.
In order for You not to lose Your authentication session in the system, You first need to save your JWT token to some storage on the client: localStorage or sessionStorage or as well as token data can be saved in cookies.
For to work of the application will be optimally, You also need to save the token in the store of Nuxt. (Vuex)
If You save Your token only in srore of Nuxt and use only state, then every time You refresh the page, Your token will be reset to zero, since the state will not have time to initialize. Therefore, you are redirected to the page /login.
To prevent this from happening, after you save Your token to some storage, You need to read it and reinitialize it in the special method nuxtServerInit(), in the universal mode his will be work on the server side the very first. (Nuxt2)
Then, accordingly, You use Your token when sending requests to the api server, adding to each request that requires authorization, a header of the Authorization type.
Since Your question is specific to the Nuxt2 version, for this version a working code example using cookies to store the token would be:
/store/auth.js
import jwtDecode from 'jwt-decode'
export const state = () => ({
token: null
})
export const getters = {
isAuthenticated: state => Boolean(state.token),
token: state => state.token
}
export const mutations = {
SET_TOKEN (state, token) {
state.token = token
}
}
export const actions = {
autoLogin ({ dispatch }) {
const token = this.$cookies.get('jwt-token')
if (isJWTValid(token)) {
dispatch('setToken', token)
} else {
dispatch('logout')
}
},
async login ({ commit, dispatch }, formData) {
const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
dispatch('setToken', token)
},
logout ({ commit }) {
this.$axios.setToken(false)
commit('SET_TOKEN', null)
this.$cookies.remove('jwt-token')
},
setToken ({ commit }, token) {
this.$axios.setToken(token, 'Bearer')
commit('SET_TOKEN', token)
this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
// <-- above use, for example, moment or add function that will computed date
}
}
/**
* Check valid JWT token.
*
* #param token
* #returns {boolean}
*/
function isJWTValid (token) {
if (!token) {
return false
}
const jwtData = jwtDecode(token) || {}
const expires = jwtData.exp || 0
return new Date().getTime() / 1000 < expires
}
/store/index.js
export const state = () => ({
// ... Your state here
})
export const getters = {
// ... Your getters here
}
export const mutations = {
// ... Your mutations here
}
export const actions = {
nuxtServerInit ({ dispatch }) { // <-- init auth
dispatch('auth/autoLogin')
}
}
/middleware/isGuest.js
export default function ({ store, redirect }) {
if (store.getters['auth/isAuthenticated']) {
redirect('/admin')
}
}
/middleware/auth.js
export default function ({ store, redirect }) {
if (!store.getters['auth/isAuthenticated']) {
redirect('/login')
}
}
/pages/login.vue
<template>
<div>
<!-- Your template here-->
</div>
</template>
<script>
export default {
name: 'Login',
layout: 'empty',
middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
data () {
return {
controls: {
login: '',
password: ''
},
rules: {
login: [
{ required: true, message: 'login is required', trigger: 'blur' }
],
password: [
{ required: true, message: 'password is required', trigger: 'blur' },
{ min: 6, message: 'minimum 6 length', trigger: 'blur' }
]
}
}
},
head: {
title: 'Login'
},
methods: {
onSubmit () {
this.$refs.form.validate(async (valid) => { // <-- Your validate
if (valid) {
// here for example: on loader
try {
await this.$store.dispatch('auth/login', {
login: this.controls.login,
password: this.controls.password
})
await this.$router.push('/admin')
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
} finally {
// here for example: off loader
}
}
})
}
}
}
</script>
! - You must have the following packages installed:
cookie-universal-nuxt
jsonwebtoken
jwt-decode
I think you will find my answer helpful. If something is not clear, ask!
Hi I am migrating to Hapi 17 from 16. I have my routes defined in a different file which I am trying to register as a plugin. But I get a 404 when I call the API. The routes are not registered with the server.
This is my Server code.
'use strict'
const Hapi = require('hapi')
const server = new Hapi.Server({ port: 1234, host: 'localhost' });
const plugins = [{
plugin: require('vision'),
plugin: require('./methods/exampleMethod'),
plugin: require('./routes/devices')
}]
async function registerPlugin(){
await server.register(plugins)
}
registerPlugin().then( () => {server.start()})
This is my routes file devices.js:
exports.plugin = {
register: (server, options) =>
{
server.routes = [{
method: 'GET',
path: '/v1/devices',
handler: async function (request, h) {
const val = server.methods.testMethod("ankur")
const response = h.response('hello world ankur')
response.type('text/plain')
return response
}
}]
},
name: 'devices'
}
Methods file
exports.plugin = {
register: (server, options) => {
server.method(
{
name: 'testMethod',
method: function (id) {
return new Promise(function (resolve, reject) {
return resolve("Test method called")
})
}
})
},
name: "exampleMethod"
I am following the release notes for Hapi 17 and trying to register the routes as a custom plugin. However, when I hit the Get v1/devices I get a 404.
Following code for your routes file will work:
exports.plugin = {
register: (server, options) => {
server.route(
{
method: "GET",
path: "/v1/devices",
handler: async function(request, h) {
//const val = server.methods.testMethod("ankur")
const response = h.response("hello world ankur");
response.type("text/plain");
return response;
}
}
);
},
name: "devices"
};
You should call server.route() function with your route object.
If you like to register more than one function through your routes plugin use something like this:
exports.plugin = {
register: (server, options) => {
const routes = [
{
method: "GET",
path: "/v1/devices",
handler: async function(request, h) {
const response = h.response("hello world");
response.type("text/plain");
return response;
}
},
{
method: "GET",
path: "/v1/another",
handler: async function(request, h) {
const response = h.response("hello another world");
response.type("text/plain");
return response;
}
}
];
server.route(routes);
},
name: "devices"
};
Edit:
Methods plugin
exports.plugin = {
register: (server, options) => {
server.method("testMethod", async function(id) {
return "Test method called";
});
},
name: "exampleMethod"
};
Call the method:
{
method: "GET",
path: "/v1/example",
handler: async function(request, h) {
const response = await request.server.methods.testMethod();
return response;
}
}