So basically here is the scenario. The user tries to access a protected API link. During the request stage, The Next.js middleware checks if the token is correct, decodes the token and then creates a current user property with response.user and if possible, checks if the exists on the MongoDB database
This is what the middleware.js currently looks like
interface CustomNextResponse extends NextResponse {
request: {
userId: string;
};
}
export async function sign(payload: JwtPayload, secret: string): Promise<string> {
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 60 * 60; // one hour
return new SignJWT({ ...payload })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setExpirationTime(exp)
.setIssuedAt(iat)
.setNotBefore(iat)
.sign(new TextEncoder().encode(secret));
}
export async function verify(token: string, secret: string): Promise<JwtPayload> {
const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
return payload;
}
export async function middleware(request: NextRequest) {
try {
const refreshToken = request.cookies.get('X-Refresh-Token')?.value;
const requestHeaders = new Headers(request.headers);
const bearerToken = requestHeaders.get('Authorization');
if (!refreshToken) {
throw new Error('Unauthorized');
}
if (!bearerToken) {
throw new Error('Unauthorized');
}
const accessToken = bearerToken.split(' ')[1];
if (!accessToken) {
throw new Error('Unauthorized');
}
const decode: JwtPayload = (await verify(accessToken, secretKey)) as JwtPayload;
const response = NextResponse.next() as CustomNextResponse;
response.request = {
userId: decode.id
}
return response;
} catch (error) {
const errorObject = error as Error;
return new NextResponse(JSON.stringify({ success: false, message: errorObject.message }), {
status: 401,
headers: { 'content-type': 'application/json' },
});
}
}
But in the Nextapi at pages/api/me I can neither access the property with req nor res
async function handler(req: CustomNextApiRequest, res: NextApiResponse) {
//req.userId and res.userId give me undefined
Is it case the case then that middleware cant does not store custom properties to be accessed when making a request the same that Express.js does it? plus can you access MongoDB within the middleware?
Related
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' })
}
So, I am learning NodeJs by creating this backend that fetches some data from a third-party API, the API requires auth. I couldn't figure out how to avoid sending an auth request to the third-party API whenever I wanted to fetch data from it. is there any way I could store the auth state in the app?
const axios = require("axios");
const AUTH_URL = process.env.AUTH_URL;
const REPORT_BASE_URL = process.env.REPORT_BASE_URL;
const X_API_KEY = process.env.X_API_KEY;
const getCompanies = async (req, res) => {
let idToken;
// auth
const authPayload = JSON.stringify({
// ...
});
const config = {
method: "post",
// ...
};
try {
const { data } = await axios(config);
idToken = data.idToken; // set idToken necessary for fetching companies
} catch (error) {
console.log(error);
}
// get company by full text query
const { full_text_query } = req.query;
if (!full_text_query)
return res.send("No full_text_query parameter provided");
try {
const { data } = await axios.get(
`${REPORT_BASE_URL}/companies?full_text_query=${full_text_query}`,
{
headers: {
"x-api-key": X_API_KEY,
Accept: "application/json",
authorization: idToken,
},
}
);
res.status(200).json(data);
} catch (error) {
console.log(error);
}
};
module.exports = {
getCompanies,
};
You can break out a function like fetchIdToken and store a Promise that resolves with the idToken in memory.
let idTokenPromise;
async function fetchIdToken () {
if (idTokenPromise) return idTokenPromise;
return idTokenPromise = new Promise(async (resolve) => {
...
resolve(data.idToken);
})
}
You can then use await fetchIdToken() at the start of getCompanies.
You can also just store the idToken in memory. This is slightly simpler, but does mean that you can have a race-condition when multiple getCompanies requests happen at the same time:
let idToken;
async function fetchIdToken () {
if (idToken) return idToken;
...
idToken = data.idToken;
return idToken;
}
I have all my functions based views on django protected with #permission_classes([IsAuthenticated]) so I have to send a JWT as Bearer token on every request.
In the first version I was using this code:
import axios from 'axios';
import { decodeUserJWT } from '../../extras'
const user = JSON.parse(localStorage.getItem("user"));
var decoded = decodeUserJWT(user.access);
var user_id = decoded.user_id
const instance = axios.create({
baseURL: 'http://localhost:8000/api',
headers: {Authorization: 'Bearer ' + user.access},
params: {userAuth: user_id}
});
export default instance;
Everything was working fine.
But then I added interceptors so I could handle the refreshToken process:
const setup = (store) => {
axiosInstance.interceptors.request.use(
(config) => {
const token = TokenService.getLocalAccessToken();
if (token) {
// const uid = await decodeUserJWT(token);
config.headers["Authorization"] = 'Bearer ' + token;
// config.headers["userAuth"] = uid;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
const { dispatch } = store;
axiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
const originalConfig = err.config;
if (originalConfig.url !== "/auth/token/obtain/" && err.response) {
console.log("TOKEN INTERCEPTOR");
// Access Token was expired
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
try {
const rs = await axiosInstance.post("/auth/token/refresh/", {
refresh: TokenService.getLocalRefreshToken(),
});
const { access } = rs.data;
dispatch(refreshToken(access));
TokenService.updateLocalAccessToken(access);
return axiosInstance(originalConfig);
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
}
);
};
What happens?
When I add the line config.headers["userAuth"] = uid; the django server console starts showing up that when the react app tries to access the routes it gets a Not Authorized, and when I take that line off de code ... it works fine.
I also tried to pass the param userAuth in the axios.create and keep only the Bearer config inside the interpector code, but still no positive result, the code with the interpector code only works when I take off the userAuth line from axios.
Any ideia on why this is happening and how can I fix this?
I am trying to follow a fireship tutorial with oAuth2 with the code below.
All functions are initialized correctly but there was this error message when I tried to authorize the App through twitter. The error message is below.
⚠ functions: Error: Value for argument "data" is not a valid Firestore document. Cannot use "undefined" as a Firestore value (found in field "accessToken"). If you want to ignore undefined values, enable ignoreUndefinedProperties.
The code is below:
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
// Database reference
const dbRef = admin.firestore().doc('tokens/demo')
const TwitterApi = require('twitter-api-v2').default
const twitterClient = new TwitterApi({
clientId: 'aabbcc',
clientSecret: 'aabbcc',
})
const callbackURL =
'http://127.0.0.1:5001/primussoft-74a49/us-central1/callback'
// STEP 1 - Auth URL
exports.auth = functions.https.onRequest(async (request, response) => {
const { url, codeVerifier, state } = twitterClient.generateOAuth2AuthLink(
callbackURL,
{ scope: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'] }
)
// store verifier
await dbRef.set({ codeVerifier, state })
response.redirect(url)
})
// STEP 2 - Verify callback code, store access_token
exports.callback = functions.https.onRequest(async (request, response) => {
const { state, code } = request.query
const dbSnapshot = await dbRef.get()
const { codeVerifier, state: storedState } = dbSnapshot.data()
if (state !== storedState) {
return response.status(400).send('Stored tokens do not match!')
}
const {
client: loggedClient,
accessToken,
refreshToken,
} = twitterClient.loginWithOAuth2({
code,
codeVerifier,
redirectUri: callbackURL,
})
await dbRef.set({ accessToken, refreshToken })
const { data } = loggedClient.v2.me() // start using the client if you want
response.send(data)
})
// STEP 3 - Refresh tokens and post tweets
exports.tweet = functions.https.onRequest(async (request, response) => {
const { refreshToken } = dbRef.get().data()
const {
client: refreshedClient,
accessToken,
refreshToken: newRefreshToken,
} = twitterClient.refreshOAuth2Token(refreshToken)
await dbRef.set({ accessToken, refreshToken: newRefreshToken })
// const { data } = await refreshedClient.v2.tweet(
// nextTweet.data.choices[0].text
// )
const { dataone } = { id: 'testid', text: 'a tweet from me' }
response.send(dataone)
})
exports.tweet = functions.https.onRequest((request, response) => {})
I was actually trying to follow a tutorial by fireship in this link https://youtu.be/V7LEihbOv3Y
Any help would be greatly appreciated.
The refreshOAuth2Token method returns a promise as well. Try adding await as shown below:
const {
client: refreshedClient,
accessToken,
refreshToken: newRefreshToken,
} = await twitterClient.refreshOAuth2Token(refreshToken)
I initially created a little express server to run a report and file write function.
var ssrs = require('mssql-ssrs');
var fs = require('fs');
const express = require('express')
const app = express()
const port = 3001
app.get('/', (req, res) => {
reportCreation();
res.send('File Created');
})
app.get('/api', (req, res) => {
reportCreation();
res.json({'File Created': true});
})
app.listen(port, () => {
console.log(`Report Api listening at http://localhost:${port}`)
})
The function reportCreation() is an async function which gets a report from a SSRS. This works fine
async function reportCreation() {
var serverUrl = 'http://reportServerName/ReportServer/ReportExecution2005.asmx';
ssrs.setServerUrl(serverUrl);
var reportPath = '/ApplicationPortalReports/TestReportNew';
var fileType = 'word';
var parameters = { ApplicationId: 3, TrainingCardId: 267, PortalPersonId: 52 }
var auth = {
username: 'USERNAME',
password: 'PASSWORD',
domain: 'dmz'
};
try {
var report = await ssrs.reportExecution.getReportByUrl(reportPath, fileType, parameters, auth)
} catch (error) {
console.log(error);
}
console.log(report);
try {
fs.writeFile('ReportApiTest.doc', report, (err) => {
if (!err) console.log('Data written');
});
} catch (error) {
console.log(error);
}
I have been working a lot with NestJs recently and wanted to use the same function but within a NestJs service.
#Injectable()
export class AppService {
async getReport(): Promise<string> {
const serverUrl = 'http://reportServerName/ReportServer/ReportExecution2005.asmx';
ssrs.setServerUrl(serverUrl);
const reportPath = '/ApplicationPortalReports/TestReportNew';
const fileType = 'word';
// var parameters = {appId: 3, ReportInstanceId: 1 }
const parameters = {ApplicationId: 3, TrainingCardId: 267, PortalPersonId: 52 };
const auth = {
username: 'USERNAME',
password: 'PASSWORD',
domain: 'dmz'
};
try {
var report = await ssrs.reportExecution.getReportByUrl(reportPath, fileType, parameters, auth)
} catch (error) {
console.log(error);
}
console.log(report);
// excel = xlsx
// word = doc
// pdf = pdf
try {
fs.writeFile('ReportApiTest.doc', report, (err) => {
if (!err) { console.log('Data written');
return 'File Written Succesfully'}
});
} catch (error) {
console.log(error);
return 'File Write Error'
}
}
}
As you can see the files are almost identical, but when I run it through NestJs I get an error which looks like a problem with the line
var report = await ssrs.reportExecution.getReportByUrl(reportPath, fileType, parameters, auth)
not awaiting. Why does this work with Express and not NestJS? Below is the error from NestJs
buffer.js:219
throw new ERR_INVALID_ARG_TYPE(
^
TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be one of type string, Buffer, ArrayBuffer,
Array, or Array-like Object. Received type undefined
at Function.from (buffer.js:219:9)
at new Buffer (buffer.js:179:17)
at Object.createType3Message (C:\Projects\SSRS-report-api\ssrs-report-api\node_modules\httpntlm\ntlm.js:172:19)
at sendType3Message (C:\Projects\SSRS-report-api\ssrs-report-api\node_modules\httpntlm\httpntlm.js:77:23)
at Immediate._onImmediate (C:\Projects\SSRS-report-api\ssrs-report-api\node_modules\httpntlm\httpntlm.js:101:4)
within the mssql-ssrs node package the getReportByURL looks like this
async function getReportByUrl(reportPath, fileType, params, auth) {
try {
var config = {
binary: true, // very important
username: auth.userName,
password: auth.password,
workstation: auth.workstation,
domain: auth.domain,
url: soap.getServerUrl()
+ "?" + (testReportPath(reportPath).replace(/\s/g, '+'))
+ "&rs:Command=Render&rs:Format=" + reportFormat(fileType)
+ formatParamsToUrl(params)
};
} catch (err) { report.errorHandler(err) }
return new Promise((resolve, reject) => {
config.url = encodeURI(config.url);
httpntlm.post(config, function (err, res) {
if (res.statusCode === 500) { reject(res) }
if (err || res.statusCode !== 200) { reject(err) }
else { resolve(res.body) }
})
})
}
Here is the app.controller.ts
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Get()
getHello(): Promise<string> {
return this.appService.getReport();
}
}
This is not an answer for the question. But after I see your code, I can see an error you will face in future if await ssrs.reportExecution.getReportByUrl(reportPath, fileType, parameters, auth) failed. Actually you see above error because of this.
The way you used the try catch is really bad.
Here's the way I code it.
#Injectable()
export class AppService {
async getReport(): Promise<string> {
const serverUrl = 'http://reportServerName/ReportServer/ReportExecution2005.asmx';
ssrs.setServerUrl(serverUrl);
const reportPath = '/ApplicationPortalReports/TestReportNew';
const fileType = 'word';
// var parameters = {appId: 3, ReportInstanceId: 1 }
const parameters = {ApplicationId: 3, TrainingCardId: 267, PortalPersonId: 52 };
const auth = {
username: 'USERNAME',
password: 'PASSWORD',
domain: 'dmz'
};
const report = await ssrs.reportExecution.getReportByUrl(reportPath, fileType, parameters, auth)
return new Promise(function(resolve, reject) {
fs.writeFile('ReportApiTest.doc', report, , function(err) {
if (err) reject(err);
resolve("File Created");
});
});
}
And in my controller
#POST
async writeFile() {
try {
const res = await this.appService.getReport();
return res;
} catch(err) {
// handle your error
}
}
I had fudged the code in the node_module changing the userName variable to username and had not done the same in the NestJS version. I forgot I had done that so now it is working.