Skip to content
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

feat: support passing data to middleware from actions #67

Merged
merged 5 commits into from
Mar 9, 2024

Conversation

JsSusenka
Copy link
Contributor

This PR adds the ability to pass additional arguments from the action builder to the middleware. This is especially useful when you want to handle the auth logic inside the middleware and you need to check if user has specific roles or permissions to execute the function. I have created a small example of how this feature would be used.

// action.ts
export const deleteUser = action(schema, async ({ id }) => {
 // an actual logic...
}, { role: "admin" });


// safeAction.ts
export const authAction = createSafeActionClient({
  async middleware(parsedInput, additionalArguments) {
    const session = cookies().get("session")?.value;

    if (!session) {
      throw new Error("Session not found!");
    }

    // In the real world, you would check if the session is valid by querying a database.
    // We'll keep it very simple here.
    const user = await getUserFromSession(session);

    if (!user) {
      throw new Error("Session is not valid!");
    }
    
    if (additionalArguments && additionalArguments.role && additionalArguments.role !== user.role) {
      throw new Error("You do not have permission to execute this action!");
    } 

    return { user };
  },
});

Copy link

vercel bot commented Feb 21, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
next-safe-action-example-app ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 8, 2024 7:16pm
next-safe-action-website ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 8, 2024 7:16pm

@Myks92
Copy link

Myks92 commented Feb 24, 2024

It seems to me that this is not useful for middleware at all. In middleware, you can get the role of the current user from the user context, or use an additional request.

export const authAction = createSafeActionClient({
  async middleware(parsedInput) {
    const session = cookies().get('session')?.value

    if (!session) {
      throw new Error('Session not found!')
    }

    // In the real world, you would check if the session is valid by querying a database.
    // We'll keep it very simple here.
    const user = await getUserFromSession(session)

    if (!user) {
      throw new Error('Session is not valid!')
    }

    const request = ...
    const pathname = request.pathname

    if (['/admin']?.some((path) => pathname.startsWith(path)) && user.role !== 'admin') {
      throw new Error('You do not have permission to execute this action!')
    }

    return { user }
  }
})

Another question is how to send a request here. It's worth thinking about. When passing a request, there are a lot more possibilities here.

In addition to adding a request, it will be useful to connect several middlewares. Then we could separate them:

// chain middlewares
export type MiddlewareFactory = (middleware: Middleware) => Middleware

export default function middlewares(
  functions: MiddlewareFactory[] = [],
  index = 0
): Middleware {
  const current = functions[index]

  if (current !== undefined) {
    const next = middlewares(functions, index + 1)
    return current(next)
  }

  return () => ...
}
const authenticate: MiddlewareFactory = (next) => {
  return async (request, _next: ) => {
    const pathname = request.nextUrl.pathname
    if (['/admin']?.some((path) => pathname.startsWith(path))) {
      const token = await getToken({ req: request })
      if (token === null) {
        throw new Error('You do not have permission to execute this action!')
      }
      if (!token.roles.includes('ROLE_ADMIN')) { 
        throw new Error('You do not have permission to execute this action!')
      }
    }
    return await next(request, _next)
  }
}

export default authenticate

But the additional properties are useful in the action itself:

// SignUp.tsx
export default function SignUp({ createUser }: Props) {
  return (
    <button onClick={async () => {
        const result = await createUser({ name: 'Max' }, { subscribe: true });
      }}>
      Sign Up
    </button>
  );
}
// action.ts
export const createUser = action(schema, async (data: Data, context: Context) => {
 const { name } = data
 const { subsctribe } = context
});

We can also consider such examples:

// changeRole action.ts
export const changeRole =  action(schema, async (data: Data, { id: string }): Promise<void> => {
  await api.patch(`/admin/users/${id}/change-role`, data)
  revalidatePath('/admin/users')
})
// activate action.ts
export const activate =  action(id: string): Promise<void> => {
  await api.patch(`/admin/users/${id}/activate`)
  revalidatePath('/admin/users')
})

@JsSusenka, @TheEdoRan, maybe we should take this into account right away?

@JsSusenka
Copy link
Contributor Author

I don´t think that you fully understood what the purpose of this PR was. In my example I´m already getting user role from user context. I don´t think that it is sufficient, to just get the required permissions from the request url. For example:

// action.ts
export const deleteUser = action(schema, async ({ id }) => {
 // an actual logic...
}, {
 role: "admin", // role name
 permissions: ["user.delete", "user.edit", "administrator"] // complex permissions
});

you can also pass complex permission array that is indeed very much useful when dealing with dashboards where you need fine-grained control over what users can do.

@Myks92 feel free to adjust your solution, so it would fit my use case.

@Myks92
Copy link

Myks92 commented Feb 24, 2024

In general, this is the case. But then we need to improve middleware. There are many unaccounted-for factors in the current offer. For example:

  1. What if I want to check whether a post (in blog) action with the status === "active" can be performed? We need an entity query to make a decision.
  2. What if I don't want to duplicate the check? After all, if tomorrow permissions are not "administrator", but "admin", then you will have to change a lot of places.
  3. If you pass parameters there for all occasions, then the function itself will become very large and difficult to read.
  4. What if you need the current active user and a bunch of different permissions. You won't be getting the current user in every middleware. There will be too many repetitions for different actions. But you can make a context for a specific action.

That's just part of what comes to mind.

@JsSusenka it can be solved like this:

//auth-action.ts
export const authAction = createSafeActionClient({
  async middleware(parsedInput) {
    const session = cookies().get('session')?.value

    if (!session) {
      throw new Error('Session not found!')
    }

    // In the real world, you would check if the session is valid by querying a database.
    // We'll keep it very simple here.
    const user = await getUserFromSession(session)

    if (!user) {
      throw new Error('Session is not valid!')
    }

    return { user }
  }
})
// action.ts
export const deleteUser = authAction(schema, async ({ id }, context) => {

  //context from middleware
  const { user } = context

  if (user.role !== 'admin') {
    throw Error('Access denided')
  }
  if (!user.permissions.includes('user.delete')) {
    throw Error('Access denided')
  }
  if (user.id !== id) {
    throw Error('Access denided')
  }

  // an actual logic...

  //context from action
  const { redirect } = context

  if (redirect) {
    redirect(redirect)
  }
});

deleteUser(schema, data, { redirect: "/admin" })

@JsSusenka
Copy link
Contributor Author

I would like to react on the points you mentioned above.

  1. What if I want to check whether a post (in blog) action with the status === "active" can be performed? We need an entity query to make a decision.

You indeed need to fetch the database to get the blog post based on the id that is being sent as a payload to the function. This point seems completely irrelevant to this PR.

  1. What if I don't want to duplicate the check? After all, if tomorrow permissions are not "administrator", but "admin", then you will have to change a lot of places.

The same issue happens in you first suggested approach. However, there are many ways of preventing this issue. For the sake of simplicity of this example, I had used plain string.

if (['/admin']?.some((path) => pathname.startsWith(path)) && user.role !== 'admin') { // this is even more unmaintable code since you are hardcoding it into the middleware. 
 throw new Error('You do not have permission to execute this action!')
}
  1. If you pass parameters there for all occasions, then the function itself will become very large and difficult to read.

I certainly agree with this issue, but that has an easy solution. You can define the additional arguments like you would the schema outside of the parameters of the function and then pass the variable in to the function. I´ve provided an example down below.

const additionalArguments = {
 role: "admin", // role name
 permissions: ["user.delete", "user.edit", "administrator"] // complex permissions
} // same as you define the action schema outside of the function to make it more readable

export const deleteUser = action(schema, async ({ id }) => {
 // an actual logic...
}, additionalArguments );
  1. What if you need the current active user and a bunch of different permissions. You won't be getting the current user in every middleware. There will be too many repetitions for different actions. But you can make a context for a specific action.

Maybe if you can be more specific and add a example for this point, because I´m sorry, but I´m not getting what you mean by it. Since server actions tends to mutate state, querying the the user from the database on every request is very much needed, because you need to ensure that the user is logged in and that he possess the permissions to execute that action.

That's just part of what comes to mind.

@JsSusenka it can be solved like this:

//auth-action.ts
export const authAction = createSafeActionClient({
  async middleware(parsedInput) {
    const session = cookies().get('session')?.value

    if (!session) {
      throw new Error('Session not found!')
    }

    // In the real world, you would check if the session is valid by querying a database.
    // We'll keep it very simple here.
    const user = await getUserFromSession(session)

    if (!user) {
      throw new Error('Session is not valid!')
    }

    return { user }
  }
})
// action.ts
export const deleteUser = authAction(schema, async ({ id }, context) => {

  //context from middleware
  const { user } = context

  if (user.role !== 'admin') {
    throw Error('Access denided')
  }
  if (!user.permissions.includes('user.delete')) {
    throw Error('Access denided')
  }
  if (user.id !== id) { // what is this supposed to be doing?
    throw Error('Access denided')
  }

  // an actual logic...

  //context from action
  const { redirect } = context

  if (redirect) {
    redirect(redirect)
  }
});

deleteUser(schema, data, { redirect: "/admin" })

Don´t get me wrong, I sure can implement the role checking logic in every action that requires some elevated permissions than the basic logged in user has, but this approach is very tedious when dealing with large application. Please correct me if I´m wrong.

@Myks92
Copy link

Myks92 commented Feb 24, 2024

@JsSusenka

If I may, I will comment only on this:

Don´t get me wrong, I sure can implement the role checking logic in every action that requires some elevated permissions than the basic logged in user has, but this approach is very tedious when dealing with large application. Please correct me if I´m wrong

  1. If you have a large and complex application, you will divide it into different parts: admin, profile, crm. Often, it is enough to check one role in such applications and full access is open. If everything is much more complicated for you, think about it.
  2. Middleware should work for all actions, or for a group of actions. At the same time, the action group should not be small, as you are trying to use it. The big actions are AdminAction, profileAction, clientAction and so on. At the same time, this may not apply to permissions and roles at all. For example, you need to convert the entire body from camelCase to snake_case before sending it to a third-party api, or you may need to make some kind of filter on all input data. So middleware is something more than just access rights.
  3. You were able to improve your code by putting additionalArguments in const, then what prevents you from doing this with a general access check and putting it in a separate function? Then your action will not have any changes on the outside. Your API will remain the same. After all, you said yourself that you have a large application. And in a large application, changing this to all server actions calls is much more difficult than placing a check inside a function. Especially if you have one server action used more than once. This is also worth thinking about. It looks like a contradiction.
  4. Your rights verification rules depend on where the server action is called. If we use the same server action (deleteUser) in several places, then we have a chance that someone will work and someone will not. Only because we passed the wrong arguments. Moreover, the editUser for an administrator should not be flexible and used for an employee. Even though the data in them does not differ now. We must make every challenge self-sufficient. Because over time, an employee may need to deny editing access to users who were created more than 30 minutes ago.

and SOLID will answer the rest.

@JsSusenka
Copy link
Contributor Author

  1. If you have a large and complex application, you will divide it into different parts: admin, profile, crm. Often, it is enough to check one role in such applications and full access is open. If everything is much more complicated for you, think about it.

Lets say, that I divide my application according to the structure. Can you please provide a example on how would I handle permission checking in every action?

  1. Middleware should work for all actions, or for a group of actions. At the same time, the action group should not be small, as you are trying to use it. The big actions are AdminAction, profileAction, clientAction and so on. At the same time, this may not apply to permissions and roles at all. For example, you need to convert the entire body from camelCase to snake_case before sending it to a third-party api, or you may need to make some kind of filter on all input data. So middleware is something more than just access rights.

I´m sorry but I haven´t got a clue, what you meant by this point. Can you please explain it more or provide a small code example? This point seems to me like it is completely irrelevant to his PR once again.

  1. You were able to improve your code by putting additionalArguments in const, then what prevents you from doing this with a general access check and putting it in a separate function? Then your action will not have any changes on the outside. Your API will remain the same. After all, you said yourself that you have a large application. And in a large application, changing this to all server actions calls is much more difficult than placing a check inside a function. Especially if you have one server action used more than once. This is also worth thinking about. It looks like a contradiction.

This is exactly what middleware is used for. As you stated, I cloud have implementčerd the whole auth logic inside another function. This is exactly a middleware. This function will be called before every actual logic in the server action. Why should I implement it myself, when this library already offers very powerful way of handling middlewares? With the small change that is being introduced in this PR the middlewares become more powerful.

  1. Your rights verification rules depend on where the server action is called. If we use the same server action (deleteUser) in several places, then we have a chance that someone will work and someone will not. Only because we passed the wrong arguments. Moreover, the editUser for an administrator should not be flexible and used for an employee. Even though the data in them does not differ now. We must make every challenge self-sufficient. Because over time, an employee may need to deny editing access to users who were created more than 30 minutes ago.

I don´t think you fully understand the root concept of server actions. You would call them only from client. Also this PR does not change the way you call actions from client. None of the additional arguments are being sent from client. These arguments are just defined right by the function in the file that is marked with the "use server" directive. Please read the docs of both server actions and this library itself so that we stand on even ground in terms of knowledge. Thank you

and SOLID will answer the rest.
Solid is being used within OOP based architectures. You need to realize, that next.js is not a OOP based and that principles and design patterns that apply to PHP often does not apply to it.

@TheEdoRan TheEdoRan changed the title feat: additional arguments for middleware feat: support passing data to middleware from actions Mar 8, 2024
@TheEdoRan TheEdoRan linked an issue Mar 8, 2024 that may be closed by this pull request
1 task
@TheEdoRan
Copy link
Owner

Hey @JsSusenka, thank for your code contribution, and also to @Myks92 for contributing to the discussion.

I slightly adjusted your implementation to be type safe, renamed the additionalArgs middleware argument to data, and added documentation for this feature on the website.

I ultimately think this is a good feature, since it allows granular access control of actions via the middleware function.
Please, let me know what you think as well. Thank you.

Copy link

github-actions bot commented Mar 9, 2024

🎉 This PR is included in version 6.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@vkhitev
Copy link

vkhitev commented Mar 9, 2024

Hi @JsSusenka @TheEdoRan,
This feature can be easily implemented in the user space. Because of this, I think it shouldn't be a part of the library API. I'm attaching a full example showing how we can check permissions in server actions.

import { createSafeActionClient, type ServerCodeFn } from "next-safe-action"
import { type Schema } from "zod"
import { auth } from "~/auth/auth"
import { type Permission } from "~/auth/permissions"

const createActionAuth = createSafeActionClient({
  async middleware(_parsedInput) {
    const session = await auth()

    if (session === null) {
      throw new Error("Unauthenticated.")
    }

    return { session }
  },
})

type AuthMiddleware = ActionMiddleware<ActionContext<typeof createActionAuth>>

const withPermissionProtection = (permission: Permission): AuthMiddleware => {
  return (serverCode) => {
    return (parsedInput, ctx) => {
      if (!ctx.session.user.permissions.includes(permission)) {
        throw new Error("Unauthorized")
      }

      return serverCode(parsedInput, ctx)
    }
  }
}

const createUser = createActionAuth(
  userSchema,
  withPermissionProtection("users.create")(
    async (user, context) => { },
  )
)

// Some utility types, useful to create a custom middleware
type ActionMiddleware<Ctx> = <const S extends Schema, const Data>(
  serverCode: ServerCodeFn<S, Data, Ctx>,
) => ServerCodeFn<S, Data, Ctx>

type ActionContext<Action extends ReturnType<typeof createSafeActionClient>> =
  Parameters<Parameters<Action>[1]>[1]

Alternatively, to remove the differences between "main" and "additional" middlewares, we can create a wrapper on top of createSafeActionClient to allow passing a middleware function as a last argument, so that the user can compose multiple middlewares and create a new action-client per action. Brief usage example:

const createSafeActionClientWrapper = (options) => (middleware) = createSafeActionClient({ ...options, middleware })

const createSafeAction = createSafeActionClientWrapper({ handleReturnedServerError })

const withSession = (_parsedInput) => {
  const session = await auth()
  if (session === null) {
    throw new Error("Unauthenticated.")
  }
  return { session }
}

const createUser = createSafeAction(
  userSchema,
  chainAsyncSequentially(withSession, withPermissionProtection('user.create')),
  async (user, context) => {}
)

const deleteUser = createSafeAction(
  z.string(),
  // You're not obligated to run the middleware sequentially.
  // Can use any chaining strategy https://github.com/sindresorhus/promise-fun?tab=readme-ov-file#packages
  chainAsyncSequentially(withSession,
    chainAsyncParallel(
      withRoleProtection('Admin'),
      withPermissionProtection('user.delete')
    )
  ),
  async (userId, context) => {}
)

To conclude, you can use function composition for better code reusability. At least, this way your "main" middleware function is decoupled from the logic that doesn't belong to it (e.g. it doesn't know anything about permissions).

Yes, the existing library API is not very functional programming–friendly by design, thus both your and my solutions are not ideal. Yours — in the way that it makes it difficult to compose middleware, and mine that it might require wrappers and boilerplate code to achieve that composable middleware.

@Myks92
Copy link

Myks92 commented Mar 9, 2024

@vkhitev, thanks for the detailed usage example. I also believe that this feature does not apply to this package. I tried to convey this above, but they didn't hear me. For me, it's generally wild when something from the lower layer (action) suddenly transmits data to the upper layer (middleware).

There is a standard from the Web world that describes the implementation in PHP.
https://www.php-fig.org/psr/psr-15/ where RequestInterface is our serverAction...

@Myks92
Copy link

Myks92 commented Mar 9, 2024

Moreover, I don't like the design of the library in that it takes over the validation of the data. This is a different responsibility and it needs to be done differently.

// frontend/src/shared/validator

import { assert as typeSchemaValidator, type Schema, type InferIn } from '@typeschema/main'

/**
 * @throws AggregateError
 */
export default async function validate<S extends Schema>(data: InferIn<S>, schema: S): Promise<void> {
  await typeSchemaValidator(schema, data)
}
// frontend/src/app/admin/users/create/action.ts
'use server'

import endpoint from '@/auth/api/endpoints/admin/users/create'
import action from '@/infrastructure/server-action/api-adapter'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import validate from '@/shared/validator'
import { z } from '@/shared/validator/adapter/zod'

interface Data {
  lastName: string
  firstName: string
  email: string
  password: string
}

const schema = z.object({
  lastName: z.string(),
  firstName: z.string(),
  email: z.string(),
  password: z.string()
})

export default action(async (data: Data, context: undefined): Promise<void> => {
  await validate(data, schema)
  const result = await endpoint(data)
  revalidatePath('/admin/users')
  redirect(`/admin/users/${result.id}`)
})

The package has become simple:

// frontend/src/shared/action

import { isNotFoundError } from 'next/dist/client/components/not-found.js'
import { isRedirectError } from 'next/dist/client/components/redirect.js'

export type MaybePromise<T> = Promise<T> | T
export type Extend<Data> = Data extends infer U ? { [K in keyof U]: U[K] } : never

export interface Config {
  handleError?: <Data>(e: Error) => MaybePromise<ActionError | ValidationErrors<Data>>
}

export interface Response<Data, Result> {
  data?: Result
  message?: ActionError['message']
  errors?: ValidationErrors<Data>
}

export type ClientAction<Data, Context, Result> = (data: Data, context: Context) => Promise<Response<Data, Result>>
export type ServerAction<Data, Context, Result> = (data: Data, context: Context) => Promise<Result>

export interface ActionError { message: string }
export type ValidationErrors<Data> = Extend<Data>

export const DEFAULT_SERVER_ERROR = 'Server error'

export const isError = (error: unknown): error is Error => error instanceof Error

const create = (config?: Config) => {
  return <Data, Context, Result>(action: ServerAction<Data, Context, Result>): ClientAction<Data, Context, Result> => {
    return async (data, context) => {
      try {
        const result = ((await action(data, context)) ?? null) as Result
        return { data: result }
      } catch (e: unknown) {
        /**
         * next/navigation functions work by throwing an error that will be processed internally by Next.js.
         * So, in this case we need to rethrow it.
         */
        if (isRedirectError(e) || isNotFoundError(e)) {
          throw e
        }
        if (!isError(e)) {
          console.warn('Could not handle server error. Not an instance of Error: ', e)
          return { message: DEFAULT_SERVER_ERROR }
        }

        return (await config?.handleError?.(e)) ?? { message: DEFAULT_SERVER_ERROR }
      }
    }
  }
}

const action = {
  create
}

export default action

The next step is to make the first parameter an object:

Contract:

type StatusType = 'success' | 'error'

interface DefaultResult {
  status: StatusType
  message?: string
}

interface DataResult<TResult = unknown> extends DefaultResult {
  status: 'success'
  data: TResult
}

interface ErrorResult extends DefaultResult {
  status: 'error'
  error: string
  message: string
}

interface ErrorsResult<TData> extends DefaultResult {
  status: 'error'
  errors: TData extends infer U ? { [K in keyof U]: U[K] } : never
}

export function isActionResult<TResult>(result: unknown): result is DataResult<TResult> {
  return typeof result === 'object' && result !== null && 'data' in result && typeof result.data !== 'undefined'
}

export function isActionError(result: unknown): result is ErrorResult {
  return typeof result === 'object' && result !== null && 'error' in result && typeof result.error === 'string'
}

export function isActionErrors<Errors>(result: unknown): result is ErrorsResult<Errors> {
  return typeof result === 'object' && result !== null && 'errors' in result && typeof result.errors === 'object'
}

interface ActionRequest<TData = unknown, TParams = unknown> {
  data: TData
  params: TParams
}

type ActionResult<TResult = void, TErrors = unknown> = DataResult<TResult> | ErrorResult | ErrorsResult<TErrors>

type Action = <TData = unknown, TResult = void, TParams = unknown>(request: ActionRequest<TData, TParams>) => Promise<ActionResult<TResult, TData>>

Action

interface Data {
  lastName: string
  firstName: string
}

interface Result {
  id: number
}

const schema = z.object({
  lastName: z.string(),
  firstName: z.string()
})

interface Params {
  type: string
}

export async function action(request: ActionRequest<Data, Params>): Promise<ActionResult<Result, Data>> {
  try {
    await validate(request.data, schema)
  } catch (error: unknown) {
    return {
      status: 'error',
      error: 'ValidationErrors',
      message: 'Validation errors'
    } satisfies ErrorResult
  }

  if (request.params.type !== 'registration') {
    return {
      status: 'error',
      error: 'RegistrationError',
      message: 'Error'
    } satisfies ErrorResult
  }

  return {
    status: 'success',
    data: { id: 1 },
    message: 'User created'
  } satisfies DataResult<Result>
}

const request = {
  data: {
    lastName: 'Last',
    firstName: 'First'
  },
  params: {
    type: 'registration'
  }
}

Usage

const result = await create({
  data: {
    lastName: 'Last',
    firstName: 'First'
  },
  params: {
    type: 'registration'
  }
})

if (isActionErrors<Data>(result)) {
  console.log('Action errors result', result)
  // { status: 'error', error: 'ValidationErrors', message: 'Validation errors', errors: [...]} }
}
if (isActionError(result)) {
  console.log('Action error result', result)
  // { status: 'error', error: 'ValidationErrors', message: 'Validation errors' }
} else {
  console.log('Action data', result)
  // { status: 'success', id: 1 }
}

Copy link

🎉 This PR is included in version 7.0.0-next.10 🎉

The release is available on:

Your semantic-release bot 📦🚀

@TheEdoRan
Copy link
Owner

Hey @JsSusenka, @Myks92 and @vkhitev, thank you for sharing your thoughts on this feature. Please check out #88 and contribute to the discussion there, if you want.

@X-Titouan
Copy link

Hey guys :)

What is the final recommandation to check a specific permission before the action execution ?

Should I make a middleware for every permission I need to check ? Or should I find a way to create permissionGrantedAction which take an array of string (permission id/name) as a parameter ?

Thank you for your help !

@TheEdoRan
Copy link
Owner

@X-Titouan

What is the final recommandation to check a specific permission before the action execution ?

https://next-safe-action.dev/docs/recipes/extend-base-client

@X-Titouan
Copy link

No worries I have read the documentation :)

This is my base client:

safe-actions.ts:

import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from 'next-safe-action';
import { z } from 'zod';

export class ActionError extends Error {}

// Base client.
export const action = createSafeActionClient({
  handleReturnedServerError(e) {
    if (e instanceof ActionError) {
      return e.message;
    }

    return DEFAULT_SERVER_ERROR_MESSAGE;
  },
  defineMetadataSchema() {
    return z.object({
      actionName: z.string(),
    });
  },
  // Define logging middleware.
}).use(async ({ next, clientInput, metadata }) => {
  console.log('LOGGING MIDDLEWARE');

  // Here we await the action execution.
  const result = await next({ ctx: null });

  console.log('Result ->', result);
  console.log('Client input ->', clientInput);
  console.log('Metadata ->', metadata);

  // And then return the result of the awaited action.
  return result;
});

And this is my authenticatedAction:

import { auth } from '@clerk/nextjs/server';
import { z } from 'zod';

import { ActionError, action } from './safe-actions';

export const authenticatedAction = action
  .metadata({ actionName: 'authenticatedAction' })
  .schema(
    z.object({
      permission: z.string().nullable(),
    })
  )
  .use(async ({ next }) => {
    const { userId } = auth();

    if (!userId) {
      throw new ActionError('User is not authenticated');
    }

    /*
    const { permission } = clientInput as { permission: string };
    if (!permission) next({ ctx: { userId } });

    const canAccessSettings = has({ permission: permission });
    if (!canAccessSettings) {
      throw new ActionError('User permission is not granted');
    }
      */

    return next({ ctx: { userId } });
  });

Should I try to make a single middleware with a permission parameter to check if the user has the permission like I tryed above ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Pass parameter to middleware serverside? [ASK]
5 participants