TRPC Context, Auth y Private Procedures

¿Qué es la autorización?

Autorización es el proceso mediante el cual determinamos si un usuario tiene los permisos para realizar una acción. En este momento tenemos autenticación, pero requerimos que las llamadas ademas de la autenticación tengan la autorización incluida.

Adjuntar el Auth State (Estado de la Autenticación) al Context (Contexto) de TRPC

  1. Abrimos el archivo src/server/api/trpc.ts y vamos a agregar el usuario de Clerk al contexto de TRPC. Debido a que Clerk utiliza JWT, puede verificar que estas llamadas que se relizan desde el cliente, sean solicitadas realmente por el usuario que dice solicitarlas.

    import { getAuth } from '@clerk/nextjs/server'
    
    // ...
    
    export const createTRPCContext = (opts: CreateNextContextOptions) => {
        const { req } = opts
    
        const { userId } = getAuth(req)
    
        if (!userId) {
            throw new TRPCError({ code: 'UNAUTHORIZED' })
        }
    
        const context = createInnerTRPCContext({})
    
        return {
            ...context,
            userId,
        }
    }
    
  2. Una ves actualizada la función que crea el context de TRPC, vamos a crear un middleware de TRPC que force al usuario a estar autenticado y creamos un nuevo procedure privado llamado privateProcedure.

    const enforceUserIsAuth = t.middleware(async function ({
        ctx: { userId },
        next,
    }) {
        if (!userId) {
            throw new TRPCError({ code: 'UNAUTHORIZED' })
        }
    
        return next({ ctx: { currentUser: userId } })
    })
    
    export const privateProcedure = t.procedure.use(enforceUserIsAuth)
    

La librería Zod

Zod es una librería que permite realizar validaciones completas utilizando una sentencia encadenada, por ejemplo.

import { z } from 'zod'

// validar el envío de un formulario que contenga un campo "content" el cual es:
// - tipo string
// - contiene solo emojis
// - entre 1 y 280 caracteres
z.object({
    content: z.string().emoji().min(1).max(280),
})

Implementar el método create para el router posts

Ahora utilizando zod y el privateprocedure que acabamos de crear, implementamos el método create dentro del router posts.

import { z } from 'zod'
// ...

export const postsRouter = createTRPCRouter({
    // ...

    create: privateProcedure
        .input(
            z.object({
                content: z.string().emoji().min(1).max(280),
            })
        )
        .mutation(async ({ ctx, input }) => {
            const authorId = ctx.userId

            await ctx.prisma.post.create({
                data: {
                    content: input.content,
                    authorId,
                },
            })
        }),
})

Invocar el método create desde el componente CreatePostWizard

Implementamos la llamada al mutator posts:create() desde el componente CreatePostWizard.

const CreatePostWizard = () => {
    const { user } = useUser()
    const [input, setInput] = useState<string>('')
    const { mutate, isLoading: isPosting } = api.posts.create.useMutation({})

    if (!user) {
        return null
    }

    if (isPosting) {
        return <LoadingPage />
    }

    return (
        <div className="flex w-full gap-3">
            <Image
                src={user.imageUrl}
                width={120}
                height={120}
                alt="Profile image"
                className="h-14 w-14 rounded-full"
            />
            <input
                placeholder="Type some emojis"
                className="grow bg-transparent outline-none"
                value={input}
                onChange={(e) => setInput(e.target.value)}
            />
            <button onClick={() => mutate({ content: input })}>Post</button>
        </div>
    )
}

Hemos complementado el código de manera que cuando se presione el botón se invoque el mutator create, sin embargo para poder ver el resultado aún es necesario refrescar la página.

Invalidar state para el query getAll

El listado de posts esta asociado al query posts:getAll, para que este se refresque automáticamente, necesitamos invalidarlo cuando el proceso del mutator posts:create se complete exitosamente, así como en cualquier otro caso en el cual se altere el número de posts en la base de datos.

Para ello vamos a especificar el método onSuccess del mutator posts:create.

const ctx = api.useContext()
// ...

const { mutate, isLoading: isPosting } = api.posts.create.useMutation({
    onSuccess: () => {
        setInput('')
        void ctx.posts.getAll.invalidate()
    },
})

Aquí lo que hemos hecho es invalidar el query posts:getAll de manera que al completar el envío de un nuevo post, se ejecute el query nuevamente.

Asegurarse del ordenamiento de los posts

Como deseamos que el orden de los posts sea del mas reciente al mas antiguo, vamos a actualizar también el query para que siga este ordenamiento.

// src/server/api/routers/post.ts

export const postsRouter = createTRPCRouter({
    getAll: publicProcedure.query(async ({ ctx }) => {
        const posts = await ctx.prisma.post.findMany({
            take: 100,
            orderBy: {
                createdAt: 'desc',
            },
        })

        // ...

        return updatedPosts
    }),

    // ...
})

Hemos podido utilizar el context de trpc para incluir dentro de este la información de la sesión del usuario, así como la librería Zod para realizar validaciones de los datos de enviados desde un formulario.