What's the best pattern to execute a server action from a client button in terms of safety?
Answered
Order posted this in #help-forum
OrderOP
So I'm building a messenger app and in my app I have a friends list that consists of 4 different categories and I'm using a server action to populate each category and another server action for the buttons that control users in set categories. For example in the "other users" category I have an Add Friend button next to the user's name that calls a server action and the server action adds an outgoing friend request in my db. However for the server action to run properly I need both the logged in user's username and the potential friend's username. This comes with the issue that there's no real safe way to pass both parameters to the Button so that the button can pass them to the server action. So what I'm doing is getting the logged in user's name from the session inside the server action and I'm only getting the potential friend's username through the client button's props. Otherwise a malicious client can potentially send friend requests from an user that's not himself. But.. since I have 4 different categories in the friends list (friends,outgoing requests,incoming requests and other users) I have to query the db and get the logged in user's session for every category effectively making the same query 4 times. I know this has gotten quite long and convoluted but I'm just stuck and can't find an effective pattern to solve this.
Answered by B33fb0n3
ok, you have multiple ways:
1. (not recommended) Create one whole function and call this functions once and pass all the stuff down
2. (recommended) Put your
You know how to put it inside a React.Cache?
1. (not recommended) Create one whole function and call this functions once and pass all the stuff down
2. (recommended) Put your
validateRequest() function side a React.cache. Like that the request is cached and deduped. It's only cached for the render. So if you reload the page, the old cache is gone. So you have one request to validateRequest and the rest is just your cache.You know how to put it inside a React.Cache?
40 Replies
I like to use
Let me know, if that helps
next-safe-actions for that. You define the error handling and schema and then you can work with specific hooks and can ensure safety. Take a look at it here: https://next-safe-action.dev/Let me know, if that helps
OrderOP
how does this prevent the potentially malicious client from switching his username to another and execute the server action from their behalf. When I said "safety" I didn't mean type safety. I'm already using zod for validation o n both the client and the server in all my serious projects.
oh, you check the auth inside your server action. Check if he is allowed to execute the action and then you are good to go 👍
OrderOP
that's what I'm doing. It seems like every abstraction in nextjs comes at the cost of increased DB load.
of course your need to check somehow if he is allowed to do that action and how you do that is your goal. To not make everytime a call to the database, I like to use RBAC. You know RBAC?
OrderOP
The issue is not with roles. Because this server action (making a friend request) can be executed by all registered users. The issue is that the friendRequest server action takes 2 params - the username of the user who's making the request and the username of their potential friend. So if the user's clever they can manipulate the first param and exchange their username with someone else's.
So for me to avoid that instead of getting the user's username through the button props and sending it to the server action I'm actually getting the user object from the auth session and getting their username from there.
ok, so you already solved that issue by
... getting their username from there.Maybe I am blind, but where's the issue?
The user can't change their session, because it's signed by the server. So if he change it to a new (other) value, it's not signed anymore. When it's not signed, it can't be validated. And without validation, no action.
OrderOP
the issue's that i'm using a session based auth system and whenever you get the session you're querying the database. So I'm querying the db 4 times for each of my categories buttons whenever a user loads their friends page.
oh that's a problem. You don't use jwt based auth system?
Why you using session based auth system?
OrderOP
for things like real-time role based authroization
chatGPT suggest I can make a higher order function in my server actions that gets the user object once and pass it to all other server actions
IDK if that'll work will give it a shot
@Order chatGPT suggest I can make a higher order function in my server actions that gets the user object once and pass it to all other server actions
and why don't you call it once, when you execute the server action? The server action is on your server, so it can access server resources
OrderOP
I am calling it once, the thing is it's not 1 server action
one sec
yea, please share some code 

OrderOP
export const acceptFriendship = async (friendUserName: string) => {
const { user } = await validateRequest();
const loggedInUserName = user?.username;
const updatedFriendship = await prisma.friendship.updateMany({
where: {
username: friendUserName,
friendUserName: loggedInUserName,
accepted: false,
},
data: {
accepted: true,
},
});
if (updatedFriendship.count === 0) {
throw new Error("No pending friendship request found to accept");
}
return updatedFriendship;
};export async function getAllFriends() {
const { user } = await validateRequest();
const loggedInUserName = user?.username;
// Fetch all accepted friendships where the logged-in user is involved
const friendships = await prisma.friendship.findMany({
where: {
accepted: true,
OR: [
{ username: loggedInUserName },
{ friendUserName: loggedInUserName },
],
},
select: {
id: true,
username: true,
friendUserName: true,
createdAt: true,
},
});
// Extract friend usernames
const friends = friendships.map((friendship) => {
return friendship.username === loggedInUserName
? friendship.friendUserName
: friendship.username;
});
// Fetch user details for all friends
const friendsDetails = await prisma.user.findMany({
where: {
username: {
in: friends,
},
},
select: {
id: true,
username: true,
email: true,
},
});
return friendsDetails;
}you can use 3 `
to make a big code block
to make a big code block
OrderOP
ah ok
I have a character limit cuz not a nitro user
then paste it here: https://paste.gg/
this is my dbActions.ts file
as you can see there's a lot of const {user} = validateRequest() which is lucia auth's way of getting the user from the session
and in the friends page I'm calling those functions to get the lists
const outgoingRequests = await getOutgoingRequests();
const incomingRequests = await getIncomingFriendRequests();
const friendsList = await getAllFriends();and then in the client buttons I have to call the rest.. man it's so difficult to explain
you explained good. I think I understood now. Because you call these function on the page you have redundant api calls. So you have muliple call to
validateRequest() just to get the user and you want to have only one call. Have I understood that correctly?OrderOP
yeah
ok, you have multiple ways:
1. (not recommended) Create one whole function and call this functions once and pass all the stuff down
2. (recommended) Put your
You know how to put it inside a React.Cache?
1. (not recommended) Create one whole function and call this functions once and pass all the stuff down
2. (recommended) Put your
validateRequest() function side a React.cache. Like that the request is cached and deduped. It's only cached for the render. So if you reload the page, the old cache is gone. So you have one request to validateRequest and the rest is just your cache.You know how to put it inside a React.Cache?
Answer
OrderOP
I'll look it up
can I do this in the server actions file tho?
yes, the function then will be converted to a normal function
Keep in mind to update to canary first
OrderOP
I'll do some digging around will mark this as a solution for now
thanks a lot
happy to help. Ping me in this thread if you need more help with this issue