feat(WEB-127): add trpc to handle requests both serverside and clientside

This commit is contained in:
Simon Emanuelsson
2024-03-20 16:39:11 +01:00
parent 2087ac6c91
commit ec4da5798b
31 changed files with 422 additions and 40 deletions

View File

@@ -0,0 +1,21 @@
import { redirect } from "next/navigation"
import { auth } from "@/auth"
import type { LangParams, LayoutArgs } from "@/types/params"
export default async function ProtectedLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
const session = await auth()
/**
* Fallback to make sure every route nested in the
* protected route group is actually protected.
*/
if (!session) {
return redirect(`/${params.lang}/login`)
}
return <>{children}</>
}

View File

@@ -1,4 +1,4 @@
import { auth } from "@/auth"
import { serverClient } from "@/lib/trpc/server"
import Overview from "@/components/MyPages/Blocks/Overview"
import OverviewMobile from "@/components/MyPages/Blocks/Overview/Mobile"
@@ -10,12 +10,11 @@ import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params"
export default async function MyPage({ params }: PageArgs<LangParams>) {
const session = await auth()
console.log({ session })
const data = await serverClient().user.get()
return (
<section className={styles.container}>
<header className={styles.header}>
<Title uppercase>Good morning {session?.user?.name ?? "[NAME]"}</Title>
<Title uppercase>Good morning {data.name}</Title>
</header>
<section className={styles.blocks}>
<Overview />

View File

@@ -1,5 +1,10 @@
import { SessionProvider } from "next-auth/react"
import { auth } from "@/auth"
import AdobeScript from "@/components/Current/AdobeScript"
import Script from "next/script"
import TrpcProvider from "@/lib/trpc/Provider"
import VwoScript from "@/components/Current/VwoScript"
import type { Metadata } from "next"
@@ -12,10 +17,11 @@ export const metadata: Metadata = {
title: "Scandic Hotels New Web",
}
export default function RootLayout({
export default async function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
const session = await auth()
return (
<html lang={params.lang}>
<head>
@@ -38,7 +44,9 @@ export default function RootLayout({
<VwoScript />
</head>
<body>
{children}
<SessionProvider session={session}>
<TrpcProvider>{children}</TrpcProvider>
</SessionProvider>
<Script id="page-tracking">{`
typeof _satellite !== "undefined" && _satellite.pageBottom();
`}</Script>

View File

@@ -1,6 +1,6 @@
import { notFound } from "next/navigation"
import { request } from "@/lib/request"
import { request } from "@/lib/graphql/request"
import { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql"
import { GetCurrentBlockPageTrackingData } from "@/lib/graphql/Query/CurrentBlockPageTrackingData.graphql"

View File

@@ -1,4 +1,4 @@
import { previewRequest } from "@/lib/previewRequest"
import { previewRequest } from "@/lib/graphql/previewRequest"
import { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql"
import type { PageArgs, LangParams, PreviewParams } from "@/types/params"

View File

@@ -0,0 +1,15 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"
import { appRouter } from "@/server"
import { createContext } from "@/server/context"
async function handler(req: Request) {
return fetchRequestHandler({
createContext,
endpoint: "/api/trpc",
req,
router: appRouter,
})
}
export { handler as GET, handler as POST }

34
auth.ts
View File

@@ -54,17 +54,31 @@ export const config = {
async signIn(...args) {
console.log("****** SIGN IN *******")
console.log(args)
console.log("****** END - SIGN IN *******")
return true
},
async session(...args) {
async session({ session, token, user }) {
console.log("****** SESSION *******")
console.log(args)
return args[0].session
console.log({ session })
console.log({ token })
console.log({ user })
console.log("****** END - SESSION *******")
if (session.user) {
return {
...session,
user: {
...session.user,
id: token.sub,
},
}
}
return session
},
async redirect({ baseUrl, url }) {
console.log("****** REDIRECT *******")
console.log({ url })
console.log({ baseUrl })
console.log({ url })
console.log("****** END - REDIRECT *******")
// Allows relative callback URLs
if (url.startsWith("/")) {
return `${baseUrl}${url}`
@@ -76,15 +90,15 @@ export const config = {
},
async authorized({ auth, request }) {
console.log("****** AUTHORIZED *******")
console.log({ request, auth })
// const { pathname } = request.nextUrl
// if (pathname === "/middleware-example") return !!auth
console.log({ auth })
console.log({ request })
console.log("****** END - AUTHORIZED *******")
return true
},
async jwt({ session, token, trigger }) {
console.log("****** JWT *******")
// if (trigger === "update") token.name = session.user.name
console.log({ token, trigger, session })
console.log({ session, token, trigger })
console.log("****** END - JWT *******")
return token
},
},
@@ -92,10 +106,12 @@ export const config = {
async signIn(...args) {
console.log("#### SIGNIN EVENT ARGS ######")
console.log(args)
console.log("#### END - SIGNIN EVENT ARGS ######")
},
async session(...args) {
console.log("#### SESSION EVENT ARGS ######")
console.log(args)
console.log("#### END - SESSION EVENT ARGS ######")
},
},
logger: {

View File

@@ -1,4 +1,4 @@
import { request } from "@/lib/request"
import { request } from "@/lib/graphql/request"
import { GetFooter } from "@/lib/graphql/Query/Footer.graphql"
import Image from "@/components/Image"

View File

@@ -1,8 +1,11 @@
import { languages } from "@/constants/languages"
import { batchRequest } from "@/lib/batchRequest"
import { request } from "@/lib/request"
import { batchRequest } from "@/lib/graphql/batchRequest"
import { request } from "@/lib/graphql/request"
import { GetHeader } from "@/lib/graphql/Query/Header.graphql"
import { GetDaDeEnUrls, GetFiNoSvUrls } from "@/lib/graphql/Query/LanguageSwitcher.graphql"
import {
GetDaDeEnUrls,
GetFiNoSvUrls,
} from "@/lib/graphql/Query/LanguageSwitcher.graphql"
import { homeHrefs } from "@/constants/homeHrefs"
import { env } from "@/env/server"
@@ -24,7 +27,11 @@ export default async function Header({ lang, uid }: LangParams & HeaderProps) {
uid,
}
const { data } = await request<HeaderQueryData>(GetHeader, { locale: lang }, { tags: [`header-${lang}`] })
const { data } = await request<HeaderQueryData>(
GetHeader,
{ locale: lang },
{ tags: [`header-${lang}`] }
)
const { data: urls } = await batchRequest<LanguageSwitcherQueryData>([
{
document: GetDaDeEnUrls,
@@ -44,10 +51,12 @@ export default async function Header({ lang, uid }: LangParams & HeaderProps) {
const currentLanguage = languages[lang]
const homeHref = homeHrefs[env.NODE_ENV][lang]
const { frontpage_link_text, logoConnection, menu, top_menu } = data.all_header.items[0]
const { frontpage_link_text, logoConnection, menu, top_menu } =
data.all_header.items[0]
const logo = logoConnection.edges?.[0]?.node
const topMenuMobileLinks = top_menu.links.filter(link => link.show_on_mobile)
.sort((a, b) => a.sort_order_mobile < b.sort_order_mobile ? 1 : -1)
const topMenuMobileLinks = top_menu.links
.filter((link) => link.show_on_mobile)
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
return (
<header className={styles.header} role="banner">

View File

@@ -1,4 +1,4 @@
import { request } from "@/lib/request"
import { request } from "@/lib/graphql/request"
import { GetMyPagesLogo } from "@/lib/graphql/Query/Logo.graphql"
import Image from "@/components/Image"

View File

@@ -4,15 +4,19 @@ import { request } from "./request"
import type { Data } from "@/types/request"
import type { BatchRequestDocument } from "graphql-request"
export async function batchRequest<T>(queries: (BatchRequestDocument & NextFetchRequestConfig)[]): Promise<Data<T>> {
export async function batchRequest<T>(
queries: (BatchRequestDocument & NextFetchRequestConfig)[]
): Promise<Data<T>> {
try {
const response = await Promise.allSettled(
queries.map(query => request<T>(query.document, query.variables, { tags: query.tags }))
queries.map((query) =>
request<T>(query.document, query.variables, { tags: query.tags })
)
)
let data = {} as T
const reasons = []
response.forEach(res => {
response.forEach((res) => {
if (res.status === "fulfilled") {
data = Object.assign({}, data, res.value.data)
} else {

View File

@@ -2,10 +2,10 @@ import "server-only"
import { request as graphqlRequest } from "graphql-request"
import { env } from "@/env/server"
import ContentstackLivePreview from "@contentstack/live-preview-utils"
import type { Data } from "@/types/request"
import type { DocumentNode } from "graphql"
import ContentstackLivePreview from "@contentstack/live-preview-utils"
export async function previewRequest<T>(
query: string | DocumentNode,

View File

@@ -7,7 +7,10 @@ import type { Data } from "@/types/request"
import type { DocumentNode } from "graphql"
const client = new GraphQLClient(env.CMS_URL, {
fetch: cache(async function (url: URL | RequestInfo, params: RequestInit | undefined) {
fetch: cache(async function (
url: URL | RequestInfo,
params: RequestInit | undefined
) {
return fetch(url, params)
}),
})
@@ -18,7 +21,6 @@ export async function request<T>(
next?: NextFetchRequestConfig
): Promise<Data<T>> {
try {
if (next) {
client.requestConfig.next = next
}

29
lib/trpc/Provider.tsx Normal file
View File

@@ -0,0 +1,29 @@
"use client"
import { useState } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { httpBatchLink } from "@trpc/client"
import { api } from "./client"
import { transformer } from "@/server/transformer"
function initializeTrpcClient() {
return api.createClient({
links: [
httpBatchLink({
transformer,
url: "http://localhost:3000/api/trpc",
}),
],
})
}
export default function TrpcProvider({ children }: React.PropsWithChildren) {
const [queryClient] = useState(() => new QueryClient({}))
const [trpcClient] = useState(() => initializeTrpcClient())
return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</api.Provider>
)
}

5
lib/trpc/client.ts Normal file
View File

@@ -0,0 +1,5 @@
import { createTRPCReact } from "@trpc/react-query"
import type { AppRouter } from "@/server"
export const api = createTRPCReact<AppRouter>({})

9
lib/trpc/server.ts Normal file
View File

@@ -0,0 +1,9 @@
import { appRouter } from "@/server"
import { createContext } from "@/server/context"
import { createCallerFactory } from "@/server/trpc"
const createCaller = createCallerFactory(appRouter)
export function serverClient() {
return createCaller(createContext())
}

View File

@@ -5,7 +5,6 @@ import { auth } from "@/auth"
import { findLocale } from "@/constants/locales"
import { pageNames } from "@/constants/myPages"
import { apiAuthPrefix } from "@/routes/api"
import { protectedRoutes } from "@/routes/protected"
import type { NextAuthRequest } from "next-auth"
@@ -69,11 +68,6 @@ const authedMiddleware = auth(authedMiddlewareFunction)
export async function middleware(request: NextRequest) {
const { nextUrl } = request
const isApiRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
if (isApiRoute) {
return NextResponse.next()
}
const locale = findLocale(nextUrl.pathname)
if (!locale) {
//return <LocalePicker />
@@ -105,5 +99,5 @@ export const config = {
* public routes inside middleware.
* (https://clerk.com/docs/quickstarts/nextjs?utm_source=sponsorship&utm_medium=youtube&utm_campaign=code-with-antonio&utm_content=12-31-2023#add-authentication-to-your-app)
*/
matcher: ["/((?!.+\\.[\\w]+$|_next|en/test).*)", "/", "/(api)(.*)"],
matcher: ["/((?!.+\\.[\\w]+$|_next|en/test|api|trpc).*)"],
}

99
package-lock.json generated
View File

@@ -13,6 +13,10 @@
"@netlify/plugin-nextjs": "^5.0.0-beta.9",
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.2",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@trpc/client": "^11.0.0-next-beta.318",
"@trpc/react-query": "^11.0.0-next-beta.318",
"@trpc/server": "^11.0.0-next-beta.318",
"class-variance-authority": "^0.7.0",
"dayjs": "^1.11.10",
"graphql": "^16.8.1",
@@ -24,6 +28,7 @@
"react-dom": "^18",
"react-feather": "^2.0.10",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -2288,6 +2293,30 @@
}
}
},
"node_modules/@tanstack/query-core": {
"version": "5.28.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.6.tgz",
"integrity": "sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.28.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.6.tgz",
"integrity": "sha512-/DdYuDBSsA21Qbcder1R8Cr/3Nx0ZnA2lgtqKsLMvov8wL4+g0HBz/gWYZPlIsof7iyfQafyhg4wUVUsS3vWZw==",
"dependencies": {
"@tanstack/query-core": "5.28.6"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -2297,6 +2326,40 @@
"node": ">= 6"
}
},
"node_modules/@trpc/client": {
"version": "11.0.0-next-beta.318",
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-next-beta.318.tgz",
"integrity": "sha512-R3IlUZqN3WKNNWsayMiVP6JqWVdyNSuwQmBQY7VqVepUBV210uo4GoFLv2vmmegOlHzgx9IUZG7u1grN1v1nAg==",
"funding": [
"https://trpc.io/sponsor"
],
"peerDependencies": {
"@trpc/server": "11.0.0-next-beta.318+e9899d002"
}
},
"node_modules/@trpc/react-query": {
"version": "11.0.0-next-beta.318",
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.0.0-next-beta.318.tgz",
"integrity": "sha512-T6l4+OuOkE4yylUqtT5EiLXo0M5+XW6YaKqnTCMkELQrrPmq3Ok0eKfhWy1Xk+ayx02POO9fTzNhITz3BID21Q==",
"funding": [
"https://trpc.io/sponsor"
],
"peerDependencies": {
"@tanstack/react-query": "^5.25.0",
"@trpc/client": "11.0.0-next-beta.318+e9899d002",
"@trpc/server": "11.0.0-next-beta.318+e9899d002",
"react": ">=18.2.0",
"react-dom": ">=18.2.0"
}
},
"node_modules/@trpc/server": {
"version": "11.0.0-next-beta.318",
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-next-beta.318.tgz",
"integrity": "sha512-lxwWfqgv3LvIfhagCElDtNEY6C2sQU3o43OH0vn9hQ+7o9j+JHwEZjZz1ixvm3wUZvwZ68LheXuKL9RdVn1d4w==",
"funding": [
"https://trpc.io/sponsor"
]
},
"node_modules/@types/cacheable-request": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -3759,6 +3822,20 @@
"node": ">= 0.8"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/copy-to": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz",
@@ -6483,6 +6560,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -9919,6 +10007,17 @@
}
}
},
"node_modules/superjson": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz",
"integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -21,6 +21,10 @@
"@netlify/plugin-nextjs": "^5.0.0-beta.9",
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.2",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "^5.28.6",
"@trpc/client": "^11.0.0-next-beta.318",
"@trpc/react-query": "^11.0.0-next-beta.318",
"@trpc/server": "^11.0.0-next-beta.318",
"class-variance-authority": "^0.7.0",
"dayjs": "^1.11.10",
"graphql": "^16.8.1",
@@ -32,6 +36,7 @@
"react-dom": "^18",
"react-feather": "^2.0.10",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -1 +0,0 @@
export const apiAuthPrefix = "/api/auth"

27
server/context.ts Normal file
View File

@@ -0,0 +1,27 @@
import { auth } from "@/auth"
type CreateContextOptions = {
auth: typeof auth
}
/** Use this helper for:
* - testing, where we dont have to Mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
**/
export function createContextInner(opts: CreateContextOptions) {
return {
auth: opts.auth,
}
}
/**
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/
export function createContext() {
return createContextInner({
auth,
})
}
export type Context = ReturnType<typeof createContext>

28
server/errors.ts Normal file
View File

@@ -0,0 +1,28 @@
import { TRPCError } from "@trpc/server"
import {
TRPC_ERROR_CODES_BY_NUMBER,
TRPC_ERROR_CODES_BY_KEY,
} from "@trpc/server/rpc"
export function unauthorizedError() {
return new TRPCError({
code: TRPC_ERROR_CODES_BY_NUMBER[TRPC_ERROR_CODES_BY_KEY.UNAUTHORIZED],
message: `Authorization required!`,
})
}
export function internalServerError() {
return new TRPCError({
code: TRPC_ERROR_CODES_BY_NUMBER[
TRPC_ERROR_CODES_BY_KEY.INTERNAL_SERVER_ERROR
],
message: `Internal Server Error!`,
})
}
export function badRequestError(msg = "Bad request!") {
return new TRPCError({
code: TRPC_ERROR_CODES_BY_NUMBER[TRPC_ERROR_CODES_BY_KEY.BAD_REQUEST],
message: msg,
})
}

10
server/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import { router } from "./trpc"
/** Routers */
import { userRouter } from "./routers/user"
export const appRouter = router({
user: userRouter,
})
export type AppRouter = typeof appRouter

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { userQueryRouter } from "./query"
export const userRouter = mergeRouters(userQueryRouter)

View File

@@ -0,0 +1,3 @@
/**
* Add route inputs (both query & mutation)
*/

View File

@@ -0,0 +1,3 @@
/**
* Add User mutations
*/

View File

@@ -0,0 +1,27 @@
import { z } from "zod"
/**
* Return value from jsonplaceholder.com/users/1
* Add proper user object expectation when fetching
* from Scandic API
*/
export const getUserSchema = z.object({
address: z.object({
city: z.string(),
geo: z.object({}),
street: z.string(),
suite: z.string(),
zipcode: z.string(),
}),
company: z.object({
bs: z.string(),
catchPhrase: z.string(),
name: z.string(),
}),
email: z.string().email(),
id: z.number(),
name: z.string(),
phone: z.string(),
username: z.string(),
website: z.string(),
})

View File

@@ -0,0 +1,25 @@
import { badRequestError, internalServerError } from "@/server/errors"
import { protectedProcedure, router } from "@/server/trpc"
import { getUserSchema } from "./output"
export const userQueryRouter = router({
get: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
const response = await fetch(
"https://jsonplaceholder.typicode.com/users/1",
{
cache: "no-store",
}
)
if (!response.ok) {
throw internalServerError()
}
const json = await response.json()
const validJson = getUserSchema.parse(json)
if (!validJson) {
throw badRequestError()
}
return validJson
}),
})

2
server/transformer.ts Normal file
View File

@@ -0,0 +1,2 @@
import superjson from "superjson"
export const transformer = superjson

35
server/trpc.ts Normal file
View File

@@ -0,0 +1,35 @@
import { initTRPC } from "@trpc/server"
import { env } from "@/env/server"
import { transformer } from "./transformer"
import { unauthorizedError } from "./errors"
import type { Context } from "./context"
import type { Meta } from "@/types/trpc/meta"
const t = initTRPC.context<Context>().meta<Meta>().create({ transformer })
export const { createCallerFactory, mergeRouters, router } = t
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(async function (opts) {
const authRequired = opts.meta?.authRequired ?? true
const session = await opts.ctx.auth()
if (authRequired) {
if (!session?.user) {
throw unauthorizedError()
}
} else {
if (env.NODE_ENV === "development") {
console.info(
`❌❌❌❌ You are opting out of authorization, if its done on purpose maybe you should use the publicProcedure instead. ❌❌❌❌`
)
console.info(`path: ${opts.path} | type: ${opts.type}`)
}
}
return opts.next({
ctx: {
session,
},
})
})

3
types/trpc/meta.ts Normal file
View File

@@ -0,0 +1,3 @@
export type Meta = {
authRequired?: boolean
}