diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx new file mode 100644 index 000000000..60abbe46f --- /dev/null +++ b/app/[lang]/(live)/(protected)/layout.tsx @@ -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>) { + 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} +} diff --git a/app/[lang]/(live)/(protected)/my-pages/page.tsx b/app/[lang]/(live)/(protected)/my-pages/page.tsx index bc6b05c65..24024a00b 100644 --- a/app/[lang]/(live)/(protected)/my-pages/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/page.tsx @@ -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) { - const session = await auth() - console.log({ session }) + const data = await serverClient().user.get() return (
- Good morning {session?.user?.name ?? "[NAME]"} + Good morning {data.name}
diff --git a/app/[lang]/(live)/layout.tsx b/app/[lang]/(live)/layout.tsx index 9deddef70..73299de11 100644 --- a/app/[lang]/(live)/layout.tsx +++ b/app/[lang]/(live)/layout.tsx @@ -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>) { + const session = await auth() return ( @@ -38,7 +44,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/app/[lang]/(live-current)/current-content-page/page.tsx b/app/[lang]/(live-current)/current-content-page/page.tsx index 5e6ec11a8..fd876a1f5 100644 --- a/app/[lang]/(live-current)/current-content-page/page.tsx +++ b/app/[lang]/(live-current)/current-content-page/page.tsx @@ -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" diff --git a/app/[lang]/(preview-current)/preview-current/page.tsx b/app/[lang]/(preview-current)/preview-current/page.tsx index 21fd9b89c..9943944e9 100644 --- a/app/[lang]/(preview-current)/preview-current/page.tsx +++ b/app/[lang]/(preview-current)/preview-current/page.tsx @@ -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" diff --git a/app/api/trpc/[trpc]/route.ts b/app/api/trpc/[trpc]/route.ts new file mode 100644 index 000000000..2f357d41f --- /dev/null +++ b/app/api/trpc/[trpc]/route.ts @@ -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 } diff --git a/auth.ts b/auth.ts index fdb7e65d0..495c050c8 100644 --- a/auth.ts +++ b/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: { diff --git a/components/Current/Footer/index.tsx b/components/Current/Footer/index.tsx index 5edce4f69..34d02009f 100644 --- a/components/Current/Footer/index.tsx +++ b/components/Current/Footer/index.tsx @@ -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" diff --git a/components/Current/Header/index.tsx b/components/Current/Header/index.tsx index 8e01bd761..37072d125 100644 --- a/components/Current/Header/index.tsx +++ b/components/Current/Header/index.tsx @@ -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(GetHeader, { locale: lang }, { tags: [`header-${lang}`] }) + const { data } = await request( + GetHeader, + { locale: lang }, + { tags: [`header-${lang}`] } + ) const { data: urls } = await batchRequest([ { 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 (
diff --git a/components/MyPages/Header/Logo/index.tsx b/components/MyPages/Header/Logo/index.tsx index dd6145b4e..2cdf51cdf 100644 --- a/components/MyPages/Header/Logo/index.tsx +++ b/components/MyPages/Header/Logo/index.tsx @@ -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" diff --git a/lib/batchRequest.ts b/lib/graphql/batchRequest.ts similarity index 67% rename from lib/batchRequest.ts rename to lib/graphql/batchRequest.ts index 319db94bd..8d108146e 100644 --- a/lib/batchRequest.ts +++ b/lib/graphql/batchRequest.ts @@ -4,15 +4,19 @@ import { request } from "./request" import type { Data } from "@/types/request" import type { BatchRequestDocument } from "graphql-request" -export async function batchRequest(queries: (BatchRequestDocument & NextFetchRequestConfig)[]): Promise> { +export async function batchRequest( + queries: (BatchRequestDocument & NextFetchRequestConfig)[] +): Promise> { try { const response = await Promise.allSettled( - queries.map(query => request(query.document, query.variables, { tags: query.tags })) + queries.map((query) => + request(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 { diff --git a/lib/previewRequest.ts b/lib/graphql/previewRequest.ts similarity index 100% rename from lib/previewRequest.ts rename to lib/graphql/previewRequest.ts index eb655147e..45ef318a6 100644 --- a/lib/previewRequest.ts +++ b/lib/graphql/previewRequest.ts @@ -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( query: string | DocumentNode, diff --git a/lib/request.ts b/lib/graphql/request.ts similarity index 94% rename from lib/request.ts rename to lib/graphql/request.ts index 8562c03a4..44a4dff9c 100644 --- a/lib/request.ts +++ b/lib/graphql/request.ts @@ -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( next?: NextFetchRequestConfig ): Promise> { try { - if (next) { client.requestConfig.next = next } diff --git a/lib/trpc/Provider.tsx b/lib/trpc/Provider.tsx new file mode 100644 index 000000000..1d877f43d --- /dev/null +++ b/lib/trpc/Provider.tsx @@ -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 ( + + {children} + + ) +} diff --git a/lib/trpc/client.ts b/lib/trpc/client.ts new file mode 100644 index 000000000..6bed0d895 --- /dev/null +++ b/lib/trpc/client.ts @@ -0,0 +1,5 @@ +import { createTRPCReact } from "@trpc/react-query" + +import type { AppRouter } from "@/server" + +export const api = createTRPCReact({}) diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts new file mode 100644 index 000000000..1563af0c8 --- /dev/null +++ b/lib/trpc/server.ts @@ -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()) +} diff --git a/middleware.ts b/middleware.ts index 83457883c..9323e94d4 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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 @@ -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).*)"], } diff --git a/package-lock.json b/package-lock.json index 3edbb099a..6c1d81634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index c69c19d2f..c5ac39993 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/routes/api.ts b/routes/api.ts deleted file mode 100644 index 041300c66..000000000 --- a/routes/api.ts +++ /dev/null @@ -1 +0,0 @@ -export const apiAuthPrefix = "/api/auth" diff --git a/server/context.ts b/server/context.ts new file mode 100644 index 000000000..bea59ca3f --- /dev/null +++ b/server/context.ts @@ -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 diff --git a/server/errors.ts b/server/errors.ts new file mode 100644 index 000000000..56fe8db92 --- /dev/null +++ b/server/errors.ts @@ -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, + }) +} diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 000000000..0893360bd --- /dev/null +++ b/server/index.ts @@ -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 diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts new file mode 100644 index 000000000..6e21035fe --- /dev/null +++ b/server/routers/user/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { userQueryRouter } from "./query" + +export const userRouter = mergeRouters(userQueryRouter) diff --git a/server/routers/user/input.ts b/server/routers/user/input.ts new file mode 100644 index 000000000..a14d3d404 --- /dev/null +++ b/server/routers/user/input.ts @@ -0,0 +1,3 @@ +/** + * Add route inputs (both query & mutation) + */ diff --git a/server/routers/user/mutation.ts b/server/routers/user/mutation.ts new file mode 100644 index 000000000..d7afaac42 --- /dev/null +++ b/server/routers/user/mutation.ts @@ -0,0 +1,3 @@ +/** + * Add User mutations + */ diff --git a/server/routers/user/output.ts b/server/routers/user/output.ts new file mode 100644 index 000000000..5de77f4f3 --- /dev/null +++ b/server/routers/user/output.ts @@ -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(), +}) diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts new file mode 100644 index 000000000..fbf983060 --- /dev/null +++ b/server/routers/user/query.ts @@ -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 + }), +}) diff --git a/server/transformer.ts b/server/transformer.ts new file mode 100644 index 000000000..0d8d72aeb --- /dev/null +++ b/server/transformer.ts @@ -0,0 +1,2 @@ +import superjson from "superjson" +export const transformer = superjson diff --git a/server/trpc.ts b/server/trpc.ts new file mode 100644 index 000000000..2615eae63 --- /dev/null +++ b/server/trpc.ts @@ -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().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, + }, + }) +}) diff --git a/types/trpc/meta.ts b/types/trpc/meta.ts new file mode 100644 index 000000000..e52d2bbaf --- /dev/null +++ b/types/trpc/meta.ts @@ -0,0 +1,3 @@ +export type Meta = { + authRequired?: boolean +}