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 Overview from "@/components/MyPages/Blocks/Overview"
|
||||||
import OverviewMobile from "@/components/MyPages/Blocks/Overview/Mobile"
|
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"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function MyPage({ params }: PageArgs<LangParams>) {
|
export default async function MyPage({ params }: PageArgs<LangParams>) {
|
||||||
const session = await auth()
|
const data = await serverClient().user.get()
|
||||||
console.log({ session })
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
<Title uppercase>Good morning {session?.user?.name ?? "[NAME]"}</Title>
|
<Title uppercase>Good morning {data.name}</Title>
|
||||||
</header>
|
</header>
|
||||||
<section className={styles.blocks}>
|
<section className={styles.blocks}>
|
||||||
<Overview />
|
<Overview />
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import { SessionProvider } from "next-auth/react"
|
||||||
|
|
||||||
|
import { auth } from "@/auth"
|
||||||
|
|
||||||
import AdobeScript from "@/components/Current/AdobeScript"
|
import AdobeScript from "@/components/Current/AdobeScript"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
import TrpcProvider from "@/lib/trpc/Provider"
|
||||||
import VwoScript from "@/components/Current/VwoScript"
|
import VwoScript from "@/components/Current/VwoScript"
|
||||||
|
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
@@ -12,10 +17,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Scandic Hotels New Web",
|
title: "Scandic Hotels New Web",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
|
const session = await auth()
|
||||||
return (
|
return (
|
||||||
<html lang={params.lang}>
|
<html lang={params.lang}>
|
||||||
<head>
|
<head>
|
||||||
@@ -38,7 +44,9 @@ export default function RootLayout({
|
|||||||
<VwoScript />
|
<VwoScript />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
<SessionProvider session={session}>
|
||||||
|
<TrpcProvider>{children}</TrpcProvider>
|
||||||
|
</SessionProvider>
|
||||||
<Script id="page-tracking">{`
|
<Script id="page-tracking">{`
|
||||||
typeof _satellite !== "undefined" && _satellite.pageBottom();
|
typeof _satellite !== "undefined" && _satellite.pageBottom();
|
||||||
`}</Script>
|
`}</Script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notFound } from "next/navigation"
|
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 { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql"
|
||||||
import { GetCurrentBlockPageTrackingData } from "@/lib/graphql/Query/CurrentBlockPageTrackingData.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 { GetCurrentBlockPage } from "@/lib/graphql/Query/CurrentBlockPage.graphql"
|
||||||
|
|
||||||
import type { PageArgs, LangParams, PreviewParams } from "@/types/params"
|
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) {
|
async signIn(...args) {
|
||||||
console.log("****** SIGN IN *******")
|
console.log("****** SIGN IN *******")
|
||||||
console.log(args)
|
console.log(args)
|
||||||
|
console.log("****** END - SIGN IN *******")
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
async session(...args) {
|
async session({ session, token, user }) {
|
||||||
console.log("****** SESSION *******")
|
console.log("****** SESSION *******")
|
||||||
console.log(args)
|
console.log({ session })
|
||||||
return args[0].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 }) {
|
async redirect({ baseUrl, url }) {
|
||||||
console.log("****** REDIRECT *******")
|
console.log("****** REDIRECT *******")
|
||||||
console.log({ url })
|
|
||||||
console.log({ baseUrl })
|
console.log({ baseUrl })
|
||||||
|
console.log({ url })
|
||||||
|
console.log("****** END - REDIRECT *******")
|
||||||
// Allows relative callback URLs
|
// Allows relative callback URLs
|
||||||
if (url.startsWith("/")) {
|
if (url.startsWith("/")) {
|
||||||
return `${baseUrl}${url}`
|
return `${baseUrl}${url}`
|
||||||
@@ -76,15 +90,15 @@ export const config = {
|
|||||||
},
|
},
|
||||||
async authorized({ auth, request }) {
|
async authorized({ auth, request }) {
|
||||||
console.log("****** AUTHORIZED *******")
|
console.log("****** AUTHORIZED *******")
|
||||||
console.log({ request, auth })
|
console.log({ auth })
|
||||||
// const { pathname } = request.nextUrl
|
console.log({ request })
|
||||||
// if (pathname === "/middleware-example") return !!auth
|
console.log("****** END - AUTHORIZED *******")
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
async jwt({ session, token, trigger }) {
|
async jwt({ session, token, trigger }) {
|
||||||
console.log("****** JWT *******")
|
console.log("****** JWT *******")
|
||||||
// if (trigger === "update") token.name = session.user.name
|
console.log({ session, token, trigger })
|
||||||
console.log({ token, trigger, session })
|
console.log("****** END - JWT *******")
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -92,10 +106,12 @@ export const config = {
|
|||||||
async signIn(...args) {
|
async signIn(...args) {
|
||||||
console.log("#### SIGNIN EVENT ARGS ######")
|
console.log("#### SIGNIN EVENT ARGS ######")
|
||||||
console.log(args)
|
console.log(args)
|
||||||
|
console.log("#### END - SIGNIN EVENT ARGS ######")
|
||||||
},
|
},
|
||||||
async session(...args) {
|
async session(...args) {
|
||||||
console.log("#### SESSION EVENT ARGS ######")
|
console.log("#### SESSION EVENT ARGS ######")
|
||||||
console.log(args)
|
console.log(args)
|
||||||
|
console.log("#### END - SESSION EVENT ARGS ######")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
logger: {
|
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 { GetFooter } from "@/lib/graphql/Query/Footer.graphql"
|
||||||
|
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { languages } from "@/constants/languages"
|
import { languages } from "@/constants/languages"
|
||||||
import { batchRequest } from "@/lib/batchRequest"
|
import { batchRequest } from "@/lib/graphql/batchRequest"
|
||||||
import { request } from "@/lib/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { GetHeader } from "@/lib/graphql/Query/Header.graphql"
|
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 { homeHrefs } from "@/constants/homeHrefs"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
@@ -24,7 +27,11 @@ export default async function Header({ lang, uid }: LangParams & HeaderProps) {
|
|||||||
uid,
|
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>([
|
const { data: urls } = await batchRequest<LanguageSwitcherQueryData>([
|
||||||
{
|
{
|
||||||
document: GetDaDeEnUrls,
|
document: GetDaDeEnUrls,
|
||||||
@@ -44,10 +51,12 @@ export default async function Header({ lang, uid }: LangParams & HeaderProps) {
|
|||||||
|
|
||||||
const currentLanguage = languages[lang]
|
const currentLanguage = languages[lang]
|
||||||
const homeHref = homeHrefs[env.NODE_ENV][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 logo = logoConnection.edges?.[0]?.node
|
||||||
const topMenuMobileLinks = top_menu.links.filter(link => link.show_on_mobile)
|
const topMenuMobileLinks = top_menu.links
|
||||||
.sort((a, b) => a.sort_order_mobile < b.sort_order_mobile ? 1 : -1)
|
.filter((link) => link.show_on_mobile)
|
||||||
|
.sort((a, b) => (a.sort_order_mobile < b.sort_order_mobile ? 1 : -1))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header} role="banner">
|
<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 { GetMyPagesLogo } from "@/lib/graphql/Query/Logo.graphql"
|
||||||
|
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { request } from "./request"
|
|||||||
import type { Data } from "@/types/request"
|
import type { Data } from "@/types/request"
|
||||||
import type { BatchRequestDocument } from "graphql-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 {
|
try {
|
||||||
const response = await Promise.allSettled(
|
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
|
let data = {} as T
|
||||||
const reasons = []
|
const reasons = []
|
||||||
response.forEach(res => {
|
response.forEach((res) => {
|
||||||
if (res.status === "fulfilled") {
|
if (res.status === "fulfilled") {
|
||||||
data = Object.assign({}, data, res.value.data)
|
data = Object.assign({}, data, res.value.data)
|
||||||
} else {
|
} else {
|
||||||
@@ -2,10 +2,10 @@ import "server-only"
|
|||||||
import { request as graphqlRequest } from "graphql-request"
|
import { request as graphqlRequest } from "graphql-request"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
import ContentstackLivePreview from "@contentstack/live-preview-utils"
|
||||||
|
|
||||||
import type { Data } from "@/types/request"
|
import type { Data } from "@/types/request"
|
||||||
import type { DocumentNode } from "graphql"
|
import type { DocumentNode } from "graphql"
|
||||||
import ContentstackLivePreview from "@contentstack/live-preview-utils"
|
|
||||||
|
|
||||||
export async function previewRequest<T>(
|
export async function previewRequest<T>(
|
||||||
query: string | DocumentNode,
|
query: string | DocumentNode,
|
||||||
@@ -7,7 +7,10 @@ import type { Data } from "@/types/request"
|
|||||||
import type { DocumentNode } from "graphql"
|
import type { DocumentNode } from "graphql"
|
||||||
|
|
||||||
const client = new GraphQLClient(env.CMS_URL, {
|
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)
|
return fetch(url, params)
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -18,7 +21,6 @@ export async function request<T>(
|
|||||||
next?: NextFetchRequestConfig
|
next?: NextFetchRequestConfig
|
||||||
): Promise<Data<T>> {
|
): Promise<Data<T>> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
client.requestConfig.next = 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 { findLocale } from "@/constants/locales"
|
||||||
import { pageNames } from "@/constants/myPages"
|
import { pageNames } from "@/constants/myPages"
|
||||||
|
|
||||||
import { apiAuthPrefix } from "@/routes/api"
|
|
||||||
import { protectedRoutes } from "@/routes/protected"
|
import { protectedRoutes } from "@/routes/protected"
|
||||||
|
|
||||||
import type { NextAuthRequest } from "next-auth"
|
import type { NextAuthRequest } from "next-auth"
|
||||||
@@ -69,11 +68,6 @@ const authedMiddleware = auth(authedMiddlewareFunction)
|
|||||||
export async function middleware(request: NextRequest) {
|
export async function middleware(request: NextRequest) {
|
||||||
const { nextUrl } = request
|
const { nextUrl } = request
|
||||||
|
|
||||||
const isApiRoute = nextUrl.pathname.startsWith(apiAuthPrefix)
|
|
||||||
if (isApiRoute) {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
const locale = findLocale(nextUrl.pathname)
|
const locale = findLocale(nextUrl.pathname)
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
//return <LocalePicker />
|
//return <LocalePicker />
|
||||||
@@ -105,5 +99,5 @@ export const config = {
|
|||||||
* public routes inside middleware.
|
* 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)
|
* (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",
|
"@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",
|
"@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",
|
"@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",
|
"class-variance-authority": "^0.7.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
@@ -24,6 +28,7 @@
|
|||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
|
"superjson": "^2.2.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||||
@@ -2297,6 +2326,40 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||||
@@ -3759,6 +3822,20 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/copy-to": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz",
|
||||||
@@ -6483,6 +6560,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/is-wsl": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"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",
|
"@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",
|
"@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",
|
"@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",
|
"class-variance-authority": "^0.7.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
@@ -32,6 +36,7 @@
|
|||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
|
"superjson": "^2.2.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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