How to pass a dynamic port to the Websockets-gateway in NestJS? - websocket

I wanted to dynamically set the Websockets-gateway port from config in NestJS. Below is my websockets-gateway code.
import { WebSocketGateway } from '#nestjs/websockets';
const WS_PORT = parseInt(process.env.WS_PORT);
#WebSocketGateway(WS_PORT)
export class WsGateway {
constructor() {
console.log(WS_PORT);
}
}
But the WS_PORT is always NaN.
This is my bootstrap function insdie main.ts :
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: false });
const configService = app.get(ConfigService);
initAdapters(app);
await app.listen(configService.get(HTTP_PORT), () => {
console.log('Listening on port ' + configService.get(HTTP_PORT));
});
}
Below is my app.module.ts :
#Module({
imports: [
ConfigModule.forRoot({
envFilePath: './src/config/dev.env',
isGlobal: true,
}),
RedisModule,
SocketStateModule,
RedisPropagatorModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>(JWT_SECRET_KEY),
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [WsGateway, AppService],
})
export class AppModule {}
I put a console log in the Gateway constructor to print the value of 'WS_PORT' but it's always NaN.
[Nest] 13252 - 10/04/2021, 5:05:34 PM LOG [NestFactory] Starting Nest application...
NaN
Thanks in advance.

I could not find a way to add dynamic data to the decorator. So to be able to dynamically choose the port and other configurations I had to:
Create an adapter for socket-io:
Tell NestJs to use the new adapter
SocketIoAdapter.ts
import { INestApplicationContext } from '#nestjs/common';
import { IoAdapter } from '#nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { ConfigService } from '#nestjs/config';
export class SocketIoAdapter extends IoAdapter {
constructor(
private app: INestApplicationContext,
private configService: ConfigService,
) {
super(app);
}
createIOServer(port: number, options?: ServerOptions) {
port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
const path = this.configService.get<string>('SOCKETIO.SERVER.PATH');
const origins = this.configService.get<string>(
'SOCKETIO.SERVER.CORS.ORIGIN',
);
const origin = origins.split(',');
options.path = path;
options.cors = { origin };
const server = super.createIOServer(port, options);
return server;
}
}
Now, you need to edit the main.ts to use the adapter
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '#nestjs/config';
import { SocketIoAdapter } from './socket-io/socket-io.adapter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const hosts = configService.get<string>('CORS.HOST');
const hostsArray = hosts.split(',');
app.enableCors({
origin: hostsArray,
credentials: true,
});
//Here you use the adapter and sent the config service
app.useWebSocketAdapter(new SocketIoAdapter(app, configService));
await app.listen(4300);
}
bootstrap();
In this case I set the port and the cors origin, here an example of the conf file (using .env)
env.local
SOCKETIO.SERVER.PORT=4101
SOCKETIO.SERVER.PATH=
SOCKETIO.SERVER.CORS.ORIGIN=http://localhost:4200,http://localhost.com:8080
Here a link to config service Config Service NestJs

You can do it relatively straightforward if you decorate the Gateway before app.init is called:
Import the class in main.ts
Get an instance of your ConfigurationService
Manually call the decorator on the class with the config data
function decorateGateway(class_, config) {
// Just calling the decorator as a function with the class
// as argument does the same as `#WebSocketGateway`
WebSocketGateway({
cors: {
origin: config.get("websocket.cors.origin"),
}
})(class_)
}
async function bootstrap() {
const app = await NestFactory.create(AppModule, {});
const config = app.get(ConfigService);
decorateGateway(ChatGateway, config);
...
app.init();
}
The tricky part with a Gateway is that it starts up together with the server, and the decorator metadata needs to be applied to the class earlier than for other components. You can do this in main.ts before app.init.

port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
In my case I found its return string(from .env), port gets 'string' but not 'number',
but if put parseInt(this.configService.get<number>('SOCKETIO.SERVER.PORT'), 10);
then it is ok
Mind that socket-io ports must be the same on server & client side

Related

Accessing useAuth0 hook data via redux thunk action with axios instance

Have a bit of an issue attempting to get Auth0 info on the logged-in user with our current architecture.
We have redux with #reduxjs/toolkit & react-redux as our state management tool.
We use axios to make HTTP requests via redux-thunk actions.
And now we have a part of our application that allows users to signup/login with Auth0.
So, an example of our problem.
Currently our redux store is setup with some reducers
/* eslint-disable import/no-cycle */
import { configureStore } from '#reduxjs/toolkit';
import thunk from 'redux-thunk';
const createStore = (initialState?: any) => {
return configureStore({
reducer: {
// reducers are here
},
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
Then we attached that to a Provider at the base of our application
import React from 'react';
import { Provider } from 'react-redux';
import createStore from '../store/createStore';
const App = () => {
return (
<Provider store={createStore()}>
//
</Provider>
);
};
export default App;
We have an axios instance function that uses axios to make HTTP requests and handles errors.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
// I need to access auth0 data here
const { getAccessTokenSilently, isAuthenticated, isLoading, loginWithRedirect, user } = auth0;
if (!token) {
const tokenErr = {
title: 'Error',
message: 'Missing Authentication Token',
success: false,
};
throw tokenErr;
}
try {
let accessToken = token;
// Update authorization token if auth0 user
if(auth0) {
if(isAuthenticcation && user) accessToken = await getAccessTokenSilently({ audience });
else loginWithRedirect();
}
const result = await axios({
...requestConfig,
headers: {
...requestConfig.headers,
authorization: `Bearer ${accessToken}`,
},
});
return result;
} catch (error: any) {
if (error.response) {
if ([401, 403].includes(error.response.status)) {
window.location = '/';
}
const contentType = error?.response?.headers?.['content-type'];
const isHTMLRes = contentType && contentType.indexOf('text/html') !== -1;
const errObj = {
status: error?.response?.status,
statusText: error?.response?.statusText,
errorMessage: isHTMLRes && error?.response?.text && (await error?.response?.text()),
error,
};
throw errObj;
}
throw error;
}
};
export default axiosInstance;
This in an example of a thunk action, we would have something like this that uses the axios instance mentioned above to make the HTTP requests.
import axios, { API_URL } from '../../services/axios';
import { Result } from '../../types/test';
import { AppThunk } from '../../store/store';
import { setResults, setResultsLoading, setTableLoading } from './test.slice';
type DefaultThunk = () => AppThunk<Promise<void>>;
const getResults: DefaultThunk = () => async () => {
dispatch(setTableLoading(true));
try {
const result = await axios<Result[]>(
{
method: 'GET',
url: `${API_URL}/test`,
},
);
dispatch(setResults(result.data));
} catch (err: any) {
console.log({ err });
} finally {
dispatch(setResultsLoading(false));
dispatch(setTableLoading(false));
}
};
export default getResults;
We then dispatch our thunk actions to make HTTP requests and update reducer states in our React components.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import getResults from '../../reducers/test/test.thunk';
const TestComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getResults());
}, []);
return (
//
);
};
export default TestComponent;
My problem is that I have no idea how to integrate Auth0 gracefully into the current flow, so I do not have to make checks in every react component that uses a thunk action.
Basically I need access to values within the useAuth0 hook from #auth0/auth0-react for example getAccessTokenSilently, isAuthenticated, user & loginWithRedirect. Just to name a few.
We can't use the useAuth0 hook in the axios instance file, as it's not a react component/hook, nor is the thunk file.
So I'm not sure how and where the best place is to get the data so that it is accessible in the axios file, as aforementioned without having to pass it as an argument or something in every redux thunk action.
Perhaps we just need a different approach to the current flow of dispatch > action > axios request?
Is there any way to pass this data in as middleware to redux?
Any help would be greatly appreciated.
I don't believe you'd be able to use a middleware to "sniff" out the auth0 context value because middlewares run outside React. What I'd suggest here is to create a wrapper component that sits between the Auth0Provider and redux Provider components that accesses the auth0 context and dispatches an action to save it into the redux state where it can be selected via useSelector or accessed directly from store.getState().
Fortunately it appears the auth0 context value is already memoized here so it should be able to be directly consumed as a stable reference within the app.
Rough Example:
import { useDispatch } from 'react-redux';
import { useAuth0 } from '#auth0/auth0-react';
import { actions } from '../path/to/auth0Slice';
const Auth0Wrapper = ({ children }) => {
const dispatch = useDispatch();
const auth0 = useAuth0();
useEffect(() => {
dispatch(actions.setAuthContext(auth0));
}, [auth0]);
return children;
};
Create and export the store for consumption within the app.
Store
import { configureStore } from '#reduxjs/toolkit';
import thunk from 'redux-thunk';
import { combineReducers } from 'redux';
...
import auth0Reducer from '../path/to/auth0Slice';
...
const rootReducer = combineReducers({
auth0: auth0Reducer,
... other root state reducers ...
});
const createStore = (initialState?: any) => {
return configureStore({
reducer: rootReducer,
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
App
import Auth0Wrapper from '../path/to/Auth0Wrapper';
import createStore from '../path/to/store';
const store = createStore();
const App = () => {
return (
<Auth0Provider ......>
<Provider store={store}>
<Auth0Wrapper>
// ... JSX ...
</Auth0Wrapper>
</Provider>
</Auth0Provider>
);
};
export store;
export default App;
Create a new Auth0 state slice.
import { createSlice } from '#reduxjs/toolkit';
const auth0Slice = createSlice({
name: 'auth0',
initialState: {},
reducers: {
setAuthContext: (state, action) => {
return action.payload;
},
},
});
export const actions = {
...auth0Slice.actions,
};
export default auth0Slice.reducer;
From here you can import the exported store object and access the current state inside the axios setup.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import store from '../path/to/App';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
const { auth0 } = store.getState(); // <-- access current state from store
const {
getAccessTokenSilently,
isAuthenticated,
isLoading,
loginWithRedirect,
user
} = auth0;
...
};
The hook methods are great if you're not using redux, but since you are, the recommended approach is to use the spa js library - https://github.com/auth0/auth0-spa-js/.
Here's a code example for a rest call:
document.getElementById('call-api').addEventListener('click', async () => {
const accessToken = await auth0.getTokenSilently();
const result = await fetch('https://myapi.com', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const data = await result.json();
console.log(data);
});
https://github.com/auth0/auth0-spa-js/blob/master/EXAMPLES.md#calling-an-api
This is easily adaptable to thunks, in your case, inside of your axios instance ie:
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const accessToken = await auth0.getTokenSilently();
// handle token and request
}
The auth0 with hooks is more like a convenience library, but it's built on top of spa js.

"res.setHeader is not a function" error Google Auth Strategy in NestJS GraphQL

I've tried to implement an oauth method using GraphQL with Google auth and for some reason I'm getting the following error
"res.setHeader is not a function" from within the authenticate method in Google Strategy
I've used passport-google-oauth20 strategy
this is my google-auth.guard.ts
import { ExecutionContext, Injectable } from '#nestjs/common';
import { GqlExecutionContext } from '#nestjs/graphql';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class GoogleAuthGuard extends AuthGuard('google') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const gqlReq = ctx.getContext().req;
if (gqlReq) {
const { token } = ctx.getArgs();
gqlReq.body = { token };
return gqlReq;
}
return context.switchToHttp().getRequest();
}
}
this is my google.strategy.ts
import { PassportStrategy } from '#nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { Profile } from 'passport';
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor() {
super({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_REDIRECT_URL,
prompt: 'consent',
scope: ['email', 'profile'],
})
}
async validate(
accessToken: string,
refreshToken: string,
profile: Profile,
done: VerifyCallback,
): Promise<any> {
if (!profile) {
return done(new UnauthorizedException(), false);
}
return done(null, profile);
}
}
it's important to point out that since my app is a react SPA, the callbackURL value is the main page of the client and not another path in the server.
and the resolver which I intend to use to generate a jwt token and a refresh token, but the code never gets to this part due to the error in the strategy
#UseGuards(GoogleAuthGuard)
#Query(() => LoginResponseApi)
async googleLogin(
#Args({ name: 'token', type: () => String }) token: string,
#Req() req,
#Context() context
): Promise<LoginResponseApi> {
const res: Response = context.req.res;
const loginResponse: any = await this.authService.googleLogin(req)
const jwtToken = this.authService.createRefreshToken(loginResponse.user)
if (loginResponse.accessToken)
this.authService.sendRefreshToken(res, jwtToken)
return loginResponse;
}

Format request before send by ClientProxy

I use microservices and RabbitMQ as a transporter. My microservices communicate with each other, so one of them can send message to another one. I use the following way to send messages:
await this.client.send(SOME_COMMAND, obj).toPromise();
Now I need to format objects I send in all requests to any microservice in one place. For example add reqId, or serialize Map. Is it possible?
1. Per controller solution. here for simplicity I removed the handler part:
#Controller()
export class AppController {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) {
let send = client.send.bind(client);
client.send = function (pattern, payload) {
return send(pattern, { payload, systemWideProp: ""})
}
}
sum() {
this.client.send(COMMAND, obj)
}
}
2. This could be as a provider for injecting it on each controller you want using your rabbitmq client service:
custom-client-proxy.ts
import { Inject, Injectable } from '#nestjs/common';
import { ClientProxy } from '#nestjs/microservices';
#Injectable()
export class CustomClientProxy {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) { }
send(pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return this.client.send(pattern, payload)
}
}
app.module.ts
import { Module } from '#nestjs/common';
import { ClientsModule, Transport } from '#nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CustomClientProxy } from './custom-client-proxy';
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy] // export here for other modules of your app
})
export class AppModule { }
app.controller.ts
also any controller you have imported under app's modules
#Controller()
export class AppController {
constructor(private client: CustomClientProxy) {}
sum() {
this.client.send(COMMAND, obj)
}
}
3. When you need to use this functionality for more than one queue:
// custom-client-proxy.ts
#Injectable()
export class CustomClientProxy {
constructor() { }
send(client: ClientProxy, pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return client.send(pattern, payload)
}
}
// app.module.ts
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
},
{
name: 'MESSAGE_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'message_queue',
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy]
})
export class AppModule { }
// app.controller.ts
#Controller()
export class AppController {
constructor(
#Inject('MATH_SERVICE') private mathClient: ClientProxy,
#Inject('MESSAGE_SERVICE') private messageClient: ClientProxy,
private client: CustomClientProxy) {}
sum(a) {
return this.client.sendTo(this.mathClient, pattern, payload);
}
}
The answer is pretty simple: you need to pass custom Serializer to the ClientProviderOptions and you can implement everything there

Connect NestJS to a websocket server

How can NestJS be use as a websocket client? I want to connect to a remote websocket server as a client using NestJS, but I didn't find any information about this implementation in the framework.
As Nestjs is simply a framework for Nodejs, so you need to find an NPM package that supports Websocket. For example, I use ws with #types/ws type definition, and create a Websocket client as a Nestjs service class:
// socket-client.ts
import { Injectable } from "#nestjs/common";
import * as WebSocket from "ws";
#Injectable()
export class WSService {
// wss://echo.websocket.org is a test websocket server
private ws = new WebSocket("wss://echo.websocket.org");
constructor() {
this.ws.on("open", () => {
this.ws.send(Math.random())
});
this.ws.on("message", function(message) {
console.log(message);
});
}
send(data: any) {
this.ws.send(data);
}
onMessage(handler: Function) {
// ...
}
// ...
}
// app.module.ts
import { Module } from "#nestjs/common";
import { WSService } from "./socket-client";
#Module({
providers: [WSService]
})
export class AppModule {}
I try it by another way. I write an adapter with socket.io-client. Then use this adapter in boostrap by method useWebSocketAdapter. After that i can write handle websocket event in gateway like the way working with socket server (use decorator #SubscribeMessage)
My Adapter file
import { WebSocketAdapter, INestApplicationContext } from '#nestjs/common';
import { MessageMappingProperties } from '#nestjs/websockets'
import * as SocketIoClient from 'socket.io-client';
import { isFunction, isNil } from '#nestjs/common/utils/shared.utils';
import { fromEvent, Observable } from 'rxjs';
import { filter, first, map, mergeMap, share, takeUntil } from 'rxjs/operators';
export class IoClientAdapter implements WebSocketAdapter {
private io;
constructor(private app: INestApplicationContext) {
}
create(port: number, options?: SocketIOClient.ConnectOpts) {
const client = SocketIoClient("http://localhost:3000" , options || {})
this.io = client;
return client;
}
bindClientConnect(server: SocketIOClient.Socket, callback: Function) {
this.io.on('connect', callback);
}
bindClientDisconnect(client: SocketIOClient.Socket, callback: Function) {
console.log("it disconnect")
//client.on('disconnect', callback);
}
public bindMessageHandlers(
client: any,
handlers: MessageMappingProperties[],
transform: (data: any) => Observable<any>,
) {
const disconnect$ = fromEvent(this.io, 'disconnect').pipe(
share(),
first(),
);
handlers.forEach(({ message, callback }) => {
const source$ = fromEvent(this.io, message).pipe(
mergeMap((payload: any) => {
const { data, ack } = this.mapPayload(payload);
return transform(callback(data, ack)).pipe(
filter((response: any) => !isNil(response)),
map((response: any) => [response, ack]),
);
}),
takeUntil(disconnect$),
);
source$.subscribe(([response, ack]) => {
if (response.event) {
return client.emit(response.event, response.data);
}
isFunction(ack) && ack(response);
});
});
}
public mapPayload(payload: any): { data: any; ack?: Function } {
if (!Array.isArray(payload)) {
return { data: payload };
}
const lastElement = payload[payload.length - 1];
const isAck = isFunction(lastElement);
if (isAck) {
const size = payload.length - 1;
return {
data: size === 1 ? payload[0] : payload.slice(0, size),
ack: lastElement,
};
}
return { data: payload };
}
close(server: SocketIOClient.Socket) {
this.io.close()
}
}
main.js
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import {IoClientAdapter} from './adapters/ioclient.adapter'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new IoClientAdapter(app))
await app.listen(3006);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
then Gateway
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'socket.io';
#WebSocketGateway()
export class EventsGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('hello')
async identity(#MessageBody() data: number): Promise<number> {
console.log(data)
return data;
}
}
It a trick, but look so cool. Message handler can write more like nestjs style.

Can't access req from context

I'm using Koa.js with Apollo Server's apollo-server-koa.
I've debug the { req } and the value is undefined.
I've followed the documentation, but still got no clue.
Even if I access the req.headers.authorization and put this on HTTP Header of graphql gui:
{
"authorization": "bla"
}
the value is still undefined.
app.ts:
import cors from "#koa/cors";
import Koa from "koa";
import config from "./config/environtment";
import server from "./server";
const PORT: number = config.port;
async function bootstrap() {
try {
const app: Koa = new Koa();
server.applyMiddleware({ app });
app
.use(cors())
.listen(PORT, () =>
console.log(
`Server running on http://localhost:${PORT}${server.graphqlPath}`,
),
);
} catch (error) {
console.error(error);
}
}
bootstrap();
server.ts:
import { ApolloServer } from "apollo-server-koa";
import typeDefs from "./graphql/schema";
import resolvers from "./graphql/resolvers";
import context from "./graphql/context";
export default new ApolloServer({
typeDefs,
resolvers,
context,
});
context.ts
export default ({ req }) => {
console.log(req) // return undefined.
return {
test: "test",
};
};
The docs are specific to apollo-server, which uses apollo-server-express under the hood. I believe for apollo-server-koa, the options are passed in an object with a ctx field that holds the Koa Context. So the following should work:
export default ({ ctx }) => {
console.log(ctx.request)
console.log(ctx.response)
return {};
};

Resources