So I have recently stared to work with react, I am authenticating a user in my App component like this:
App
signIn(userData) {
console.log(userData)
//do a fetch call to get/users
axios.get('http://localhost:5000/api/users', {
auth: { //set auth headers so that userData will hold the email address and password for the authenticated user
username: userData. emailAddress,
password: userData.password
}
}).then(results => { console.log(results.data)
this.setState({
//set the authenticated user info into state
emailAddress: results.data,
password: results.data.user
});
})
}
and I also have another component called CreateCourse that allows a post request only if I provided the auth header from App,
CreateCourse
handleSubmit = event => {
event.preventDefault();
console.log(this.props)
const newCourse = {
title: this.state.title,
description: this.state.description,
estimatedTime: this.state.estimatedTime,
materialsNeeded: this.state.materialsNeeded
};
axios({
method: 'post',
url: 'http://localhost:5000/api/courses',
auth: {
username: this.props.emailAddress,
password: this.props.password
},
data: newCourse
}).then(
alert('The course has been successfully created!')
).then( () => {
const { history } = this.props;
history.push(`/`)
})
};
I was wondering if I could pass the auth header from App to the children components without using props or context api so that I don't have to manually put the auth headers on every axios request, for reference this is my repo : https://github.com/SpaceXar20/full_stack_app_with_react_and_a_rest_api_p10
I always create a singleton axios instance and set header for it after user signin successful.
let instance = null
class API {
constructor() {
if (!instance) {
instance = this
}
this.request = Axios.create({
baseURL: 'http://localhost:5000',
})
return instance
}
setToken = (accessToken) => {
this.request.defaults.headers.common.authorization = `Bearer ${accessToken}`
}
createCourses = () => this.request.post(...your post request...)
}
export default new API()
After your login successfull, you need call API.setToken(token). Then, when you call Api.createCourse(), the request will have token in headers.
singleton axios instance is the right approach . In the same pattern, use the below method .Import the file wherever required and use axiosapi.get .
const axiosConfig = {auth: {username: XXXX, password: YYYY}};
const axiosservice = axios.create(axiosConfig);
export const axiosapi = {
/**
* Describes the required parameters for the axiosapi.get request
* #param {string} url
* #param {Object} config - The configfor the get request (https://github.com/axios/axios#request-config)
*
* #returns {Promise}
*/
get: (url, config = {}, params) => {
return axiosservice.get(url, {
params,
responseType: 'json',
transformResponse: [
data => {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data;
return get(parsedData);
},
],
...config,
})
.then()
.catch(error => {
return Promise.reject(error);
});
},
}
Related
I have some code in TypeScript to process Spotify's login flow, it looks like this:
import * as React from 'react';
import '#patternfly/react-core/dist/styles/base.css';
import { useNavigate } from "react-router-dom";
import env from "ts-react-dotenv";
interface Props {
token: string;
}
const Callback: React.FC<Props> = (props: Props) => {
const { token } = props;
const searchParams = new URLSearchParams(token);
const code = searchParams.get('code');
var accessToken = '';
// Create the POST request to get the access token
const data = {
grant_type: 'authorization_code',
code: code,
redirect_uri: 'http://localhost:3000/callback',
};
const body = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
if (value){
body.append(key, value);
}
}
const request = new Request('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + btoa(env.CLIENT_ID + ':' + env.CLIENT_SECRET),
},
body: body,
});
// Send the POST request
fetch(request)
.then((response) => response.json())
.then((data) => {
console.log("Access token: " + data.access_token);
if (accessToken){
accessToken = data.access_token;
}
});
// Navigate to Home with the code
// https://github.com/react-navigation/react-navigation/issues/10803
// Redirect to Home
const navigate = useNavigate();
setTimeout(() => navigate('/home', { state: { accessToken } }), 50);
return <div>Loading... if the app doesn't work please reload</div>;
};
export default Callback;
I see that in the console there are 2 calls, the first one produces the correct result while there is somehow a second call that seems like its being made. I suspect that my post request somehow triggered another call within the callback field but I am not sure how that happened.
How can I fix this, set the correct access_token and go to the next page?
You need to put the fetch call inside a useEffect hook, with dependency array as empty, Please refer useEffect
When we run the code on local server there component will be render at least 2 time, since CRA will create the code with strict mode.
import * as React from "react";
import "#patternfly/react-core/dist/styles/base.css";
import { useNavigate } from "react-router-dom";
import env from "ts-react-dotenv";
interface Props {
token: string;
}
const Callback: React.FC<Props> = (props: Props) => {
const { token } = props;
const searchParams = new URLSearchParams(token);
const code = searchParams.get("code");
var accessToken = "";
// Create the POST request to get the access token
const data = {
grant_type: "authorization_code",
code: code,
redirect_uri: "http://localhost:3000/callback",
};
const body = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
if (value) {
body.append(key, value);
}
}
const request = new Request("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: "Basic " + btoa(env.CLIENT_ID + ":" + env.CLIENT_SECRET),
},
body: body,
});
// Send the POST request
useEffect(() => {
fetch(request)
.then((response) => response.json())
.then((data) => {
console.log("Access token: " + data.access_token);
if (accessToken) {
accessToken = data.access_token;
}
});
}, []);
// Navigate to Home with the code
// https://github.com/react-navigation/react-navigation/issues/10803
// Redirect to Home
const navigate = useNavigate();
setTimeout(() => navigate("/home", { state: { accessToken } }), 50);
return <div>Loading... if the app doesn't work please reload</div>;
};
export default Callback;
I'm trying to do a delete request. I can fetch the API route through pages/api/people/[something].js.
And this is the response I got from the browser's console.
DELETE - http://localhost:3000/api/people/6348053cad300ba679e8449c -
500 (Internal Server Error)
6348053cad300ba679e8449c is from the GET request at the start of the app.
In the Next.js docs, for example, the API route pages/api/post/[pid].js has the following code:
export default function handler(req, res) {
const { pid } = req.query
res.end(Post: ${pid})
}
Now, a request to /api/post/abc will respond with the text: Post: abc.
But from my API route pages/api/people/[something].js, something is undefined.
const { something } = req.query
UPDATED POST:
React component
export default function DatabaseTableContent(props) {
const id = props.item._id; // FROM A GET REQUEST
const hide = useWindowSize(639);
const [deletePeople] = useDeletePeopleMutation();
async function deleteHandler() {
await deletePeople(id);
}
return <Somecodes />;
}
apiSlice.js
export const apiSlice = createApi({
// reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: url }),
tagTypes: ["People"],
endpoints: (builder) => ({
getPeople: builder.query({
query: (people_id) => `/api/people/${people_id}`,
providesTags: ["People"],
}),
deletePeople: builder.mutation({
query: (studentInfo) => ({
url: `api/people/people-data/student-info/${studentInfo}`,
method: "DELETE",
headers: {
accept: "application/json",
},
}),
invalidatesTags: ["People"],
}),
}),
});
export const {
useGetPeopleQuery,
useDeletePeopleMutation,
} = apiSlice;
pages/api/people/people-data/student-info/[studentInfo].js
import { ObjectId, MongoClient } from "mongodb";
async function handler(res, req) {
const { studentInfo } = req.query; // the code stops here because "studentInfo" is undefined
const client = await MongoClient.connect(process.env.MONGODB_URI.toString());
const db = client.db("people-info");
if (req.method === "DELETE") {
try {
const deleteData = await db
.collection("student_info")
.deleteOne({ _id: ObjectId(studentInfo) });
const result = await res.json(deleteData);
client.close();
} catch (error) {
return res.status(500).json({ message: error });
}
}
}
export default handler;
The order of params passed to your handler functions needs to be reversed.
For NextJS API routes the req is the first param passed to the handler and the res param is second.
Example handler function from NextJS documentation:
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}
I have a next.js App which has a working axios call, which I am trying to refactor. I have it mostly working, but I can't get my new function to receive arguments.
This problem has two components to it, my next.js page, and the external custom module where I am writing my functions to use axios to call the YouTube API to retrieve info.
My next.js getStaticProps call looks like this. I know this is working. Note the function where I am trying to pass in the video ID. (The 'const = video' line)
export async function getStaticProps(context: any) {
// It's important to default the slug so that it doesn't return "undefined"
const { slug = "" } = context.params;
const film = await client.fetch(query, { slug });
const video = await youtube.grabVideoInfo(film.VideoID);
return {
props: {
film,
video,
},
revalidate: 10,
};
}
I have tried writing the axios call in two ways, trying to pass in the video ID as an argument. Neither of which work, and fail to call from the API, stating an invalid video ID, which means it isn't being passed in.
The first way:
const grabVideoInfo = async (videoId) => {
const videoGrab = axios.create({
baseURL: "https://www.googleapis.com/youtube/v3/videos?",
params: {
headers: { "Access-Control-Allow-Origin": "*" },
part: "snippet",
id: videoId,
key: KEY,
},
});
const query = await videoGrab.get().then(
(response) => {
return response.data.items[0];
},
(error) => {
return error.toJSON();
}
);
return query;
};
The second way:
const grabVideoInfo = async (videoId) => {
const videoGrab = axios.create({
baseURL: "https://www.googleapis.com/youtube/v3/videos?",
params: {
headers: { "Access-Control-Allow-Origin": "*" },
part: "snippet",
key: KEY,
},
});
const query = await videoGrab.get({ params: { id: videoId } }).then(
(response) => {
return response.data.items[0];
},
(error) => {
return error.toJSON();
}
);
return query;
};
And this is the fully working version that I am trying to rewrite, which is live on the app currently. This demonstrates that the getStaticProps client call is working.
export async function getStaticProps(context: any) {
// It's important to default the slug so that it doesn't return "undefined"
const { slug = "" } = context.params;
const film = await client.fetch(query, { slug });
const KEY = process.env.YOUTUBE_API_KEY;
const conn = axios.create({
baseURL: "https://www.googleapis.com/youtube/v3/",
params: {
headers: { "Access-Control-Allow-Origin": "*" },
part: "snippet",
id: film.videoID,
key: KEY,
},
});
const video = await (await conn.get("videos?")).data.items[0];
return {
props: {
film,
video,
},
revalidate: 10,
};
}
Any help is greatly appreciated. I'm really scratching my head with this one.
Ok so your refactor is accessing film.VideoId where the original uses film.videoId.
I'm setting a basic authentication on a Nuxt project with JWT token and cookies to be parsed by nuxtServerInit function.
On login with email/password, works as intended, setUser mutation is triggered and the appropriate user object is stored in state.auth.user.
On reload, nuxtServerInit will get the jwt token from req.headers.cookies, call the GET method and identify user.Works like a charm.
Problem starts when I hit the /logout endpoint. state.auth.user is set to false and Im effectively logged out... but If I refresh, I'm logged in again with the previous user data. Even if my cookies are properly empty (on below code, both user and cookie are undefined after logout and refresh, as expected)
So I really don't get why is my state.auth.user is back to its initial value...
store/index.js
import Vuex from "vuex";
import auth from "./modules/auth";
import axios from "~/plugins/axios";
const cookieparser = process.server ? require("cookieparser") : undefined;
const END_POINT = "api/users";
const createStore = () => {
return new Vuex.Store({
actions: {
async nuxtServerInit({ commit, dispatch}, { req }) {
let cookie = null;
console.log(req.headers.cookie)
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie);
try {
cookie = JSON.parse(parsed.auth);
console.log("cookie", cookie)
const {accessToken} = cookie
const config = {
headers: {
Authorization: `Bearer ${accessToken}`
}
}
const response = await axios.get(`${END_POINT}/current`, config)
const user = response.data
console.log("user nuxt server init", user)
await commit('setUser', user)
} catch (err) {
// No valid cookie found
console.log(err);
}
}
}
},
modules: {
auth
}
});
};
export default createStore;
modules/auth.js
import axios from "~/plugins/axios";
const Cookie = process.client ? require("js-cookie") : undefined;
const END_POINT = "api/users";
export default {
state: {
user: null,
errors: {}
},
getters: {
isAuth: state => !!state.user
},
actions: {
login({ commit }, payload) {
axios
.post(`${END_POINT}/login`, payload)
.then(({ data }) => {
const { user, accessToken } = data;
const auth = { accessToken };
Cookie.set("auth", auth);
commit("setUser", user);
})
.catch(e => {
const error = e;
console.log(e);
commit("setError", error);
});
},
logout({ commit }) {
axios
.post(`${END_POINT}/logout`)
.then(({ data }) => {
Cookie.remove("auth");
commit("setUser", false);
})
.catch(e => console.log(e));
},
},
mutations: {
setUser(state, user) {
state.user = user;
},
setError(state, errors) {
state.errors = errors;
}
}
};
The way I logout my user is by creating a mutation called clearToken and commit to it in the action :
State :
token: null,
Mutations :
clearToken(state) {
state.token = null
},
Actions :
logout(context) {
context.commit('clearToken')
Cookie.remove('token')
}
This way, you token state revert back to null.
I have a react component that when a checkbox is pressed, it calls a rest api, post request with a single parameter.
I put a breakpoint in the webapi and its never hit, still I get a 415 unsopported media type on the component
react js component (see onchange event)
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.ClientId,
ClientId: row.ClientId,
ClientSecret: row.ClientSecret,
Id: row.Id,
SiteCollectionTestUrl: row.SiteCollectionTestUrl,
TenantDomainUrl: row.TenantDomainUrl
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'Client Id',
dataIndex: 'ClientId',
key: 'ClientId'
},
{
title: 'Site Collection TestUrl',
dataIndex: 'SiteCollectionTestUrl',
key: 'SiteCollectionTestUrl',
},
{
title: 'Tenant DomainUrl',
dataIndex: 'TenantDomainUrl',
key: 'TenantDomainUrl',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].key != undefined){
console.log(selectedRows[0].key);
const options = {
method: 'post',
body: JSON.stringify({ clientid : selectedRows[0].key.toString() }) ,
config: {
headers: {
'Content-Type': 'application/json'
}
}
};
adalApiFetch(fetch, "/Tenant/SetTenantActive", options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
});
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
and the webapi method
[HttpPost]
[Route("api/Tenant/SetTenantActive")]
public async Task<IHttpActionResult> SetTenantActive([FromBody]string clientid)
{
var tenantStore = CosmosStoreFactory.CreateForEntity<Tenant>();
var allTenants = await tenantStore.Query().Where(x => x.TenantDomainUrl != null).ToListAsync();
foreach(Tenant ten in allTenants)
{
ten.Active = false;
await tenantStore.UpdateAsync(ten);
}
var tenant = await tenantStore.Query().FirstOrDefaultAsync(x => x.clientid == clientid);
if (tenant == null)
{
return NotFound();
}
tenant.Active = true;
var result = await tenantStore.UpdateAsync(tenant);
return Ok(result);
}
Couple of things I noticed.
You're trying to do a POST request with a JSON body. On the client, your request looks fine.
As I understand the POST body is
{ clientid: 'some-client-id' }
The interesting thing is in the web API you receive it as
public async Task<IHttpActionResult> SetTenantActive([FromBody]string clientid)
This is possibly the culprit. Your API is expecting a string as a POST body where it is a json object. Have you tried changing the type to dynamic or JObject?
So, essentially,
public async Task<IHttpActionResult> SetTenantActive([FromBody]dynamic clientRequest)
OR
public async Task<IHttpActionResult> SetTenantActive([FromBody]JObject clientRequest)
Alternately,
If you want to continue using your API as is, then you can just change the request you’re making from the client to ’some-client-id’ instead of { clientid: 'some-client-id' }
Change
const options = {
method: 'post',
body: JSON.stringify({ clientid : selectedRows[0].key.toString() }) ,
config: {
headers: {
'Content-Type': 'application/json'
}
}
};
to
const options = {
method: 'post',
body: JSON.stringify({ clientid : selectedRows[0].key.toString() }) ,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
Check your server settings. By default it should support json but its better to verify it. Also try to clear Accept header in yor api code and set to * which means all types.
Moreover check adalApiFetch method. What headers it send? Is the format of Content-Type used & set correctly?
For a simple RESTFul call like that you could follow suggestion naming conventions along with HTTP verbs that better clarifies the intention and simplify the call itself. No need to over complicate the API model for such a simple call.
Something like
[HttpPut] // Or HttpPost. PUT is usually used to update the resourcce
[Route("api/Tenant/{clientid}/Active")]
public async Task<IHttpActionResult> SetTenantActive(string clientid) {
var tenantStore = CosmosStoreFactory.CreateForEntity<Tenant>();
var allTenants = await tenantStore.Query().Where(x => x.TenantDomainUrl != null).ToListAsync();
var updates = new List<Task>();
foreach(Tenant ten in allTenants) {
ten.Active = false;
updates.Add(tenantStore.UpdateAsync(ten));
}
await Task.WhenAll(updates);
var tenant = await tenantStore.Query().FirstOrDefaultAsync(x => x.clientid == clientid);
if (tenant == null)
{
return NotFound();
}
tenant.Active = true;
var result = await tenantStore.UpdateAsync(tenant);
return Ok(result);
}
And on the client
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].key != undefined){
var clientid = selectedRows[0].key;
console.log(clientid);
var url = "/Tenant/" + clientid + "/Active"
const options = {
method: 'put'
};
adalApiFetch(fetch, url, options)
.then(response => {
if(response.status === 200){
Notification(
'success',
'Tenant set to active',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not activated',
error
);
console.error(error);
});
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
Why are you using post? From a 'REST`y point of view, it is used to create an entity (a tenant in your case).
The simple request intended can be solved via GET with the clientid as part of the route:
[HttpGet]
[Route("api/Tenant/SetTenantActive/{clientid}")]
public async Task<IHttpActionResult> SetTenantActive(string clientid)
{
// ...
}