Angular 9 - how to properly encode + sign in URL parameter - spring-boot

I spent many hours without success. I know it's a common problem, many solutions but for me works only Interceptor that I want to avoid.
My service - here I get email with plus like john.doe+100#gmail.com
#Injectable({
providedIn: 'root',
})
export class UsersHttpService {
httpParams = new HttpParams({encoder: new CustomEncoder()});
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
console.log(email); //john.doe+100#gmail.com
let parsedEmail = encodeURI(email); //one of many attempts
return this.http.delete(`${this.env.URI}/monitoring/api/v1/groups/${groupId}/users/`, {
params: {
groupId,
email: email.replace(' ', '+')
},
});
}
And my CustomEncoder:
export class CustomEncoder implements HttpParameterCodec {
encodeKey(key: string): string {
return encodeURIComponent(key);
}
encodeValue(value: string): string {
// console.log('encodeValue encodeValue');
// console.log(value);
// console.log(encodeURIComponent(value));
return encodeURIComponent(value);
}
decodeKey(key: string): string {
return decodeURIComponent(key);
}
decodeValue(value: string): string {
// console.log('decodeValue decodeValue');
// console.log(value);
// console.log(decodeURIComponent(value));
return decodeURIComponent(value);
}
}
When I send request from Angular then in the Network tab in web browser I see:
DELETE https://myapp/groups/d39a4f50-8ebd-11ea-a9ae-5103b15ad73b/users/?groupId=d39a4f50-8ebd-11ea-a9ae-5103b15ad73b&email=john.doe 100#gmail.com
with a space! What's wrong? Were is the problem? IN the console I get email with + but in Network tab without space instead of + sign.
My params are properly encoded (there is 200 status from backend (spring boot), email with +) ONLY when I use global interceptor (which should be avoided):
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpParams,
HttpRequest,
} from "#angular/common/http";
import {Injectable} from "#angular/core";
import {Observable} from "rxjs";
import {CustomEncoder} from "./customEncoder";
#Injectable()
export class EncodeHttpParamsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromString: req.params.toString(),
});
return next.handle(req.clone({params}));
}
}
Does anyone have any idea??? I tried to use:
return this.http.delete(${this.env.ORBITAL_URI}/monitoring/api/v1/groups/${groupId}/users/, {
params: {
groupId,
email: encodeURI(email) //or encodeURIComponent(email)
},
});
and then in Network tab I see something like john.doe%2B%40gmail.com but I get 500 error from backend

My solution - without any interceptor:
removeUsersFromGroup(groupId: string, email: string): Observable<any> {
const params = new HttpParams({
encoder: new CustomEncoder(),
fromObject: {
groupId,
email,
},
});
return this.http.delete(`${this.env.URI}/myapp/v1/groups/${groupId}/users/`, {
params: params,
});
}
Now it works as expected:)

Related

"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;
}

Angular, error 500 after sending the request in the header

I have a hard time passing the right angular request to the header. This is my service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpHeaders }
from '#angular/common/http';
import { Utente } from '../model/Utente ';
import { Prodotto } from '../model/Prodotto ';
import { OktaAuthService } from '#okta/okta-angular';
import { Observable, from } from 'rxjs';
import { Carrello } from '../model/Carrello ';
import { userInfo } from 'node:os';
import { getLocaleCurrencyCode } from '#angular/common';
const headers = new HttpHeaders().set('Accept', 'application/json');
#Injectable({
providedIn: 'root'
})
export class HttpClientService {
constructor(
private httpClient:HttpClient, private oktaAuth:OktaAuthService ) {}
getCarr(){
return this.httpClient.get<Carrello[]>('http://localhost:8080/prodotti/utente/vedicarrelloo', {headers} );
}
}
This is my spring method:
#Transactional(readOnly = true)
public List<Carrello> getCarrello(#AuthenticationPrincipal OidcUser utente){
Utente u= utenteRepository.findByEmail(utente.getEmail());
return carrelloRepository.findByUtente(u);
}
In console I get this error (error 500):
https://i.stack.imgur.com/BiONS.png
this error corresponds in my console to "java.lang.NullPointerException: null.
But if I access localhost: 8080, I can see the answer correctly, so I assume there is a problem in passing the request header in angular, can anyone tell me where am I wrong, please? I specify that I get this error only in the methods where the OidcUser is present, the rest works perfectly. Thank you!
You need to send an access token with your request. Like this:
import { Component, OnInit } from '#angular/core';
import { OktaAuthService } from '#okta/okta-angular';
import { HttpClient } from '#angular/common/http';
import sampleConfig from '../app.config';
interface Message {
date: string;
text: string;
}
#Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
failed: Boolean;
messages: Array<Message> [];
constructor(public oktaAuth: OktaAuthService, private http: HttpClient) {
this.messages = [];
}
async ngOnInit() {
const accessToken = await this.oktaAuth.getAccessToken();
this.http.get(sampleConfig.resourceServer.messagesUrl, {
headers: {
Authorization: 'Bearer ' + accessToken,
}
}).subscribe((data: any) => {
let index = 1;
const messages = data.messages.map((message) => {
const date = new Date(message.date);
const day = date.toLocaleDateString();
const time = date.toLocaleTimeString();
return {
date: `${day} ${time}`,
text: message.text,
index: index++
};
});
[].push.apply(this.messages, messages);
}, (err) => {
console.error(err);
this.failed = true;
});
}
}
On the Spring side, if you want it to accept a JWT, you'll need to change to use Jwt instead of OidcUser. Example here.
#GetMapping("/")
public String index(#AuthenticationPrincipal Jwt jwt) {
return String.format("Hello, %s!", jwt.getSubject());
}

JHipster spring controller with microservices

I have a JHipster gateway+microservice application. I have added a spring service with jhipster spring-controller and then edited the code like this:
#RestController
#RequestMapping("/api/data")
public class DataResource {
/**
* GET vin
*/
#GetMapping("/vin")
public ResponseEntity<Object> vin(#Valid #RequestBody String address) {
Chart3DataDTO[] data=new Chart3DataDTO[15];
for (int i=0;i<15;i++){
data[i]=new Chart3DataDTO(System.currentTimeMillis()+i, 200+i, 201+i, 202+i);
}
return ResponseEntity.ok(data);
}
For completeness, this is the DTO
public class Chart3DataDTO {
private Long xAxis;
private Integer[] yAxis=new Integer[3];
public Chart3DataDTO(Long xAxis, Integer yAxis1, Integer yAxis2, Integer yAxis3) {
this.xAxis = xAxis;
this.yAxis = new Integer[]{yAxis1, yAxis2, yAxis3};
}
public Long getxAxis() {
return xAxis;
}
public Integer[] getyAxis() {
return yAxis;
}
}
Then I have dockerized gateway and microservice, jhipster docker-compose and started all. Everything works but when the Angular frontent asks for /api/data/vin I get:
if not logged in: 401 (which is fine)
if logged in: the JHipster page 'an error has occurred', instead of returning the JSON of the DTO
What did I miss?
Also, it doesn't appear listed on the Jhipster registry API
2ND EDIT: Added client angular code
import { Injectable } from '#angular/core';
import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '#angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json',
'Access-Control-Allow-Origin':'*'
})
};
//const apiUrl = 'api/vin';
const apiUrl = '/api/data/vin';
#Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
/*
private handleError<T> (operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
*/
getInputVoltage(address: String): Observable<[]> {
return this.http.get<[]>(`${apiUrl}` + '?address=' + address,httpOptions);
}
}
And
import { Component, OnInit } from '#angular/core';
import * as Highcharts from 'highcharts';
import { ApiService } from '../api.service';
#Component({
selector: 'jhi-device-graph',
templateUrl: './device-graph.component.html',
styleUrls: ['./device-graph.component.scss']
})
export class DeviceGraphComponent implements OnInit {
Highcharts: typeof Highcharts = Highcharts;
chartOptions: Highcharts.Options = {
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: 'Voltage'
}
},
title: {
text: 'Input voltage'
},
series: [
{
data: [
[Date.UTC(2010, 0, 1), 29.9],
[Date.UTC(2010, 2, 1), 71.5],
[Date.UTC(2010, 3, 1), 106.4]
],
type: 'line',
name: 'Vin1'
},
{
data: [
[Date.UTC(2010, 0, 1), 39.9],
[Date.UTC(2010, 2, 1), 91.5],
[Date.UTC(2010, 3, 1), 96.4]
],
type: 'line',
name: 'Vin2'
}
]
};
data: String[] = [];
isLoadingResults = true;
constructor(private api: ApiService) {}
ngOnInit(): void {
this.api.getInputVoltage('10.1.30.1').subscribe(
(res: any) => {
this.data = res;
//console.log(this.data);
this.isLoadingResults = false;
},
err => {
//console.log(err);
this.isLoadingResults = false;
}
);
}
}
Your angular client sends a GET request on /api/data/vin with query parameters while your REST controller expects a request body, this can't work.
Your controller must expect a #RequestParam
Also, as the request goes through a gateway, it must be prefixed by /services and your service name, so in your case the URL is /services/graph/api/data/vin.
Also using #Valid on a String does not do anything unless you add some other validation annotations like #NotBlank or #Size(max=30)
#GetMapping("/vin")
public ResponseEntity<Object> vin(#Valid #RequestParam String address) {
#RequestBody must be used only for POST or PUT.
Assuming you are sending a parameter when you make the get call (seeing that #RequestBody address) and without seeing the logs, you can try changing the ResponseEntity<Object> to ResponseEntity<Chart3DataDTO[]>

NESTJS Gateway / Websocket - how to send jwt access_token through socket.emit

I am using the default passport jwt AuthGuard for my project. That works for my post & get routes fine when setting the authentication header.
Now I want to use Nestjs Gateways as well with socket.io on the client-side, but I don't know how to send the access_token to the gateway?
That is basically my Gateway:
#WebSocketGateway()
export class UserGateway {
entityManager = getManager();
#UseGuards(AuthGuard('jwt'))
#SubscribeMessage('getUserList')
async handleMessage(client: any, payload: any) {
const results = await this.entityManager.find(UserEntity);
console.log(results);
return this.entityToClientUser(results);
}
And on the client I'm sending like this:
this.socket.emit('getUserList', users => {
console.log(users);
this.userListSub.next(users);
});
How and where do I add the jwt access_token? The documentation of nestjs misses that point completely for Websockets. All they say is, that the Guards work exactly the same for websockets as they do for post / get etc. See here
While the question is answered, I want to point out the Guard is not usable to prevent unauthorized users from establishing a connection.
It's only usable to guard specific events.
The handleConnection method of a class annotated with #WebSocketGateway is called before canActivate of your Guard.
I end up using something like this in my Gateway class:
async handleConnection(client: Socket) {
const payload = this.authService.verify(
client.handshake.headers.authorization,
);
const user = await this.usersService.findOne(payload.userId);
!user && client.disconnect();
}
For anyone looking for a solution. Here it is:
#UseGuards(WsGuard)
#SubscribeMessage('yourRoute')
async saveUser(socket: Socket, data: any) {
let auth_token = socket.handshake.headers.authorization;
// get the token itself without "Bearer"
auth_token = auth_token.split(' ')[1];
}
On the client side you add the authorization header like this:
this.socketOptions = {
transportOptions: {
polling: {
extraHeaders: {
Authorization: 'your token', // 'Bearer h93t4293t49jt34j9rferek...'
}
}
}
};
// ...
this.socket = io.connect('http://localhost:4200/', this.socketOptions);
// ...
Afterwards you have access to the token on every request serverside like in the example.
Here also the WsGuard I implemented.
#Injectable()
export class WsGuard implements CanActivate {
constructor(private userService: UserService) {
}
canActivate(
context: any,
): boolean | any | Promise<boolean | any> | Observable<boolean | any> {
const bearerToken = context.args[0].handshake.headers.authorization.split(' ')[1];
try {
const decoded = jwt.verify(bearerToken, jwtConstants.secret) as any;
return new Promise((resolve, reject) => {
return this.userService.findByUsername(decoded.username).then(user => {
if (user) {
resolve(user);
} else {
reject(false);
}
});
});
} catch (ex) {
console.log(ex);
return false;
}
}
}
I simply check if I can find a user with the username from the decoded token in my database with my user service. I am sure you could make this implementation cleaner, but it works.
Thanks! At the end i implemented a Guard that like the jwt guard puts the user inside the request. At the end I'm using the query string method from the socket client to pass the auth token This is my implementation:
import { CanActivate, ExecutionContext, Injectable, Logger } from '#nestjs/common';
import { WsException } from '#nestjs/websockets';
import { Socket } from 'socket.io';
import { AuthService } from '../auth/auth.service';
import { User } from '../auth/entity/user.entity';
#Injectable()
export class WsJwtGuard implements CanActivate {
private logger: Logger = new Logger(WsJwtGuard.name);
constructor(private authService: AuthService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const client: Socket = context.switchToWs().getClient<Socket>();
const authToken: string = client.handshake?.query?.token;
const user: User = await this.authService.verifyUser(authToken);
client.join(`house_${user?.house?.id}`);
context.switchToHttp().getRequest().user = user
return Boolean(user);
} catch (err) {
throw new WsException(err.message);
}
}
}

The specified content type was not found - Error 611 - NativeScript

I'm following the step by step tutorial of NativeScript but I'm facing the following issue:
When I try to make a post request through my Angular service I get the following error:
JS: {"message":"The specified content type was not found.","errorCode":611}
My code looks pretty much the same as the one in the official docs from NativeScript:
import { Injectable } from '#angular/core';
import { Http, Headers, Response } from '#angular/http';
import { User } from './user';
import { Config } from '../config';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
#Injectable()
export class UserService {
constructor(private _http: Http) { }
register(user: User): Observable<Response> {
const headers = new Headers();
headers.append('Content-Type', 'application/json');
const url = `${Config.apiUrl}/Users`;
const body = JSON.stringify({
Username: user.email,
Email: user.email,
Password: user.password
});
return this._http.post(url, body, { headers })
.catch(this.handleErrors);
}
handleErrors(error: Response) {
console.error(JSON.stringify(error.json()));
return Observable.throw(error);
}
}
Component class:
signUp() {
this._user.register(this.user).subscribe(
() => {
alert('Your account has been successfully created.');
this.toggleDisplay();
},
() => {
// errors here...
alert('Unfortunately we were unable to create your account.');
}
);
}
Tested under Android and IOs, both throws the same error.
Isn't application/json the expected Content-Type in this case?
If I change the type I get another error saying that the type is missing or is invalid.
Can someone please point out what am I missing?

Resources