New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Meteor.user() does not work in webApp connect handlers #13051
Comments
Pull request to change things would only make sense if targeted against Meteor 3. |
Was there a package similar to this wherein interfacing with the rest points require some validation token to "log in" the user? |
I think this would be a nice addition to core. Curious how'd you go about implementing it. One idea would be to automatically include the loginToken inside Meteor's I shared one way to currently do it here: https://forums.meteor.com/t/call-meteor-userid-from-connecthandler/61072/2?u=jam Maybe there's an even better way. |
I have a POC. What do y'all think of this? // in 'meteor/fetch'
function isExternalUrl(url) {
return url.includes('://');
}
const originalFetch = global.fetch;
global.fetch = function(url, options = {}) {
if (!isExternalUrl(url) && Meteor.userId() && (!options.headers || !options.headers.get('authorization'))) {
options.headers = options.headers || new global.Headers();
options.headers.append('Authorization', `Bearer ${Accounts._storedLoginToken()}`);
}
return originalFetch(url, options);
};
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response; // in 'meteor/webapp' (in which case we wouldn't import WebApp but I didn't go that far in this POC)
import { WebApp } from 'meteor/webapp';
import { Accounts } from 'meteor/accounts-base';
import { DDP } from 'meteor/ddp';
// Middleware to set up DDP context for Meteor.userId()
const ddpUserContextMiddleware = (req, res, next) => {
const { authorization } = req.headers;
const [ prefix, token ] = authorization?.split(' ') || [];
const user = (prefix === 'Bearer' && token) ? Meteor.users.findOne({
'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token),
}) : undefined;
const ddpSession = {
userId: user ? user._id : undefined,
connection: {},
isSimulation: false,
};
DDP._CurrentInvocation.withValue(ddpSession, next);
};
// Apply middleware to all connectHandlers
WebApp.connectHandlers.use(ddpUserContextMiddleware); // server
WebApp.connectHandlers.use('/something', (req, res, next) => {
const userId = Meteor.userId();
const user = Meteor.user();
if (userId) {
console.log('User ID:', userId, user);
// Now you can use userId as needed
} else {
console.log('User ID not found');
}
next();
}); // client
import { fetch } from 'meteor/fetch'
fetch('/something') |
@jamauro, that will be a security issue attaching the token to meteor fetch as it is used universally (not only for your app's endpoints) |
@rj-david good point. I updated the snippet above to exclude external URLs to avoid that issue. maybe there's a better solution entirely. curious to hear what that could be. |
What is the normal use case of a meteor app using rest api instead of methods to access the server? |
It's not a bad question. It's kind of an anti-pattern imo to reach for a
|
The normal use case is a 3rd party system accessing a rest endpoint of a meteor app. With this use case, the code above has shortcomings
For the second point, this is normally the case for machine-to-machine integrations which requires long-lived access tokens like in oauth. |
One thing that I can think of is accessing user-based files. The file requires authentication for access. But instead of using DDP, use https which is more efficient in this case. (Of course, one can argue to just use S3 and use a signed url from the meteor app to access the files.) |
The primary application of our system involves creating Excel spreadsheets and leveraging AWS Lambda for tasks that require significant computational resources. We utilize Meteor for data management, but often find ourselves executing tasks outside of Node.js. This approach is taken because some processes are more efficiently handled outside the Node.js environment, especially when dealing with large volumes of data or computationally intensive tasks. This strategy allows us to optimize performance and manage our workload more effectively. import bodyParser from "body-parser";
import { Meteor } from "meteor/meteor";
import { WebApp } from "meteor/webapp";
// Assuming this is running on the server
WebApp.connectHandlers.use(bodyParser.json());
// Step 1: Initialize an object to store method names
global.MethodsList = {};
// Step 2: Create a wrapper for Meteor.methods
const originalMeteorMethods = Meteor.methods;
WebApp.connectHandlers.use("/api", async (req, res, next) => {
// Extract the Authorization header
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith("Bearer ")) {
const token = authHeader.slice(7); // Remove "Bearer " from the start
const hashedToken = Accounts._hashLoginToken(token);
// Find the user by the token
const user = await Meteor.users.findOneAsync({
"services.resume.loginTokens.hashedToken": hashedToken,
});
if (user) {
// Attach user information to the request object
req.user = user;
req.userId = user._id;
// Proceed to the next handler/middleware
next();
} else {
// Authentication failed
res.writeHead(401);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "error",
message: "Authentication failed",
}),
);
}
} else {
// No or improperly formatted Authorization header present
next();
}
});
async function responseHandler(callback, req, res) {
try {
const result = await callback(req, res);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(result));
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
if (error && error.reason) {
res.end(
JSON.stringify({
status: "ko",
reason: error.reason,
}),
);
} else {
res.end(JSON.stringify(error));
}
}
}
function createApiFunction({ url, func, method }) {
console.log("createApiFunction", url, func, method);
// Register API endpoint
if (method === "GET") {
WebApp.connectHandlers.use(`/api${url}`, async (req, res) => {
responseHandler(
async (req) => {
const userId = await req.userId;
return func.apply(this, [req.body, userId]);
},
req,
res,
);
});
} else if (method === "POST") {
WebApp.connectHandlers.use(`/api${url}`, async (req, res) => {
responseHandler(
async (req) => {
const userId = await req.userId;
return func.apply(this, [req.body, userId]);
},
req,
res,
);
});
} else if (method === "PUT") {
WebApp.connectHandlers.use(`/api${url}`, async (req, res) => {
responseHandler(
async (req) => {
const userId = await req.userId;
return func.apply(this, [req.body, userId]);
},
req,
res,
);
});
}
}
Meteor.methods = function (methods) {
const methodsToRegister = {};
Object.keys(methods).forEach((methodName) => {
// Store method names in the global object
MethodsList[methodName] = true;
if (typeof methods[methodName] === "function") {
// methodsToRegister[methodName] = methods[methodName];
methodsToRegister[methodName] = function func(...args) {
this.unblock();
args[1] = Meteor.userId();
return methods[methodName].apply(this, args);
};
} else {
methodsToRegister[methodName] = function func(...args) {
this.unblock();
args[1] = Meteor.userId();
return methods[methodName].function.apply(this, args);
};
if (methods[methodName].api) {
createApiFunction({
...methods[methodName].api,
func: methods[methodName].function,
});
}
}
});
// Call the original Meteor.methods function
originalMeteorMethods(methodsToRegister);
};
The primary objective is to expose all of our methods to a GPT4 bot as well as standarize the way of creating API like this: Meteor.methods({
helloWord: {
function: (data, userId) => {
// Meteor.userId();
console.log("hello user: ", userId);
// console.log("hello user data: ", Meteor.user());
console.log("hello", data);
return "Hello World!";
},
api: {
method: "POST",
url: "/hello",
},
ai: {
description: "use to say hello",
params: {
name: String
}
}
},
}) |
@nesbtesh, how do you handle expired tokens for this case? |
this is just a test but I will add it... @rj-david how do you suggest we handle it? |
Maybe another use-case less explored where using HTTP endpoints over the DDP is when using a Server-Side Rendering (SSR) approach for your app, not really extended approach on Meteor though. But if we take ever that direction further, having Meteor context on those endpoints would be great. I like the approach from @jamauro above. |
On top of my head, this should be through a mechanism of long-lived sessions like oauth if the access is coming from a 3rd party system without an actual logged-in user accessing the app. Although when the access is made on behalf of a logged-in user, that looks like a clear use-case for this. |
The challenge here is that the starting point in the client does not have that context, yet (client is not yet running). This is solved through the use of cookies by existing packages. |
meteor/packages/accounts-base/accounts_server.js
Line 123 in 7411b3c
Is there a reason why Meteor.user() and Meteor.userId() doesnt work with the webApp connect handlers?
Can I create a pull request to make a change?
The text was updated successfully, but these errors were encountered: