feat(WEB-127): add trpc to handle requests both serverside and clientside
This commit is contained in:
21
app/[lang]/(live)/(protected)/layout.tsx
Normal file
21
app/[lang]/(live)/(protected)/layout.tsx
Normal 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}</>
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
15
app/api/trpc/[trpc]/route.ts
Normal file
15
app/api/trpc/[trpc]/route.ts
Normal 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
34
auth.ts
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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
29
lib/trpc/Provider.tsx
Normal 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
5
lib/trpc/client.ts
Normal 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
9
lib/trpc/server.ts
Normal 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())
|
||||
}
|
||||
@@ -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
99
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const apiAuthPrefix = "/api/auth"
|
||||
27
server/context.ts
Normal file
27
server/context.ts
Normal 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
28
server/errors.ts
Normal 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
10
server/index.ts
Normal 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
|
||||
5
server/routers/user/index.ts
Normal file
5
server/routers/user/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { userQueryRouter } from "./query"
|
||||
|
||||
export const userRouter = mergeRouters(userQueryRouter)
|
||||
3
server/routers/user/input.ts
Normal file
3
server/routers/user/input.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* Add route inputs (both query & mutation)
|
||||
*/
|
||||
3
server/routers/user/mutation.ts
Normal file
3
server/routers/user/mutation.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
/**
|
||||
* Add User mutations
|
||||
*/
|
||||
27
server/routers/user/output.ts
Normal file
27
server/routers/user/output.ts
Normal 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(),
|
||||
})
|
||||
25
server/routers/user/query.ts
Normal file
25
server/routers/user/query.ts
Normal 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
2
server/transformer.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import superjson from "superjson"
|
||||
export const transformer = superjson
|
||||
35
server/trpc.ts
Normal file
35
server/trpc.ts
Normal 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
3
types/trpc/meta.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type Meta = {
|
||||
authRequired?: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user