From da343f45da7cd4cd224fc0257a23d314457039e1 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Mon, 8 Jul 2024 14:53:48 +0200 Subject: [PATCH] feat: new "safe" procedure without unauth throwing --- components/Current/Header/TopMenu/index.tsx | 7 +- components/Current/Header/index.tsx | 11 +- .../DynamicContent/OverviewTable/index.tsx | 10 +- .../Loyalty/Blocks/DynamicContent/index.tsx | 6 +- server/context.ts | 4 +- server/routers/user/query.ts | 105 +++++++++++------- server/trpc.ts | 34 +++++- types/components/current/header/mainMenu.ts | 2 +- types/components/loyalty/blocks.ts | 6 +- utils/user.ts | 1 + 10 files changed, 117 insertions(+), 69 deletions(-) diff --git a/components/Current/Header/TopMenu/index.tsx b/components/Current/Header/TopMenu/index.tsx index 69d23798e..be0e99d97 100644 --- a/components/Current/Header/TopMenu/index.tsx +++ b/components/Current/Header/TopMenu/index.tsx @@ -1,7 +1,6 @@ import { logout } from "@/constants/routes/handleAuth" import { serverClient } from "@/lib/trpc/server" -import { auth } from "@/auth" import Link from "@/components/TempDesignSystem/Link" import { getIntl } from "@/i18n" @@ -23,9 +22,7 @@ export default async function TopMenu({ lang, }: TopMenuProps) { const { formatMessage } = await getIntl() - const session = await auth() - const user = session ? await serverClient().user.get() : null - + const user = await serverClient().user.name() return (
@@ -46,7 +43,7 @@ export default async function TopMenu({ ))}
  • - {session ? ( + {user ? ( <> {user ? ( case LoyaltyComponentEnum.overview_table: - return + return default: return null } diff --git a/server/context.ts b/server/context.ts index 537719540..2c4c235ee 100644 --- a/server/context.ts +++ b/server/context.ts @@ -10,7 +10,7 @@ import { unauthorizedError } from "./errors/trpc" typeof auth type CreateContextOptions = { - auth: () => Promise + auth: () => Promise lang: Lang pathname: string uid?: string | null @@ -50,7 +50,7 @@ export function createContext() { const session = await auth() const webToken = webviewTokenCookie?.value if (!session?.token && !webToken) { - throw unauthorizedError() + return null } return session || ({ token: { access_token: webToken } } as Session) diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index c80a086c5..30a77d9c4 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -1,9 +1,13 @@ import * as api from "@/lib/api" -import { protectedProcedure, router } from "@/server/trpc" +import { + protectedProcedure, + router, + safeProtectedProcedure, +} from "@/server/trpc" import { countries } from "@/components/TempDesignSystem/Form/Country/countries" import * as maskValue from "@/utils/maskValue" -import { getMembershipCards } from "@/utils/user" +import { getMembership, getMembershipCards } from "@/utils/user" import { friendTransactionsInput, @@ -19,6 +23,37 @@ import { } from "./output" import { benefits, extendedUser, nextLevelPerks } from "./temp" +import type { Session } from "next-auth" + +async function getVerifiedUser({ session }: { session: Session }) { + const apiResponse = await api.get(api.endpoints.v1.profile, { + cache: "no-store", + headers: { + Authorization: `Bearer ${session.token.access_token}`, + }, + }) + + if (!apiResponse.ok) { + return null + } + + const apiJson = await apiResponse.json() + if (!apiJson.data?.attributes) { + console.error(`User has no data - (user: ${JSON.stringify(session.user)})`) + return null + } + + const verifiedData = getUserSchema.safeParse(apiJson.data.attributes) + if (!verifiedData.success) { + console.info( + `Failed to validate User - (User: ${JSON.stringify(session.user)})` + ) + console.error(verifiedData.error) + return null + } + return verifiedData +} + function fakingRequest(payload: T): Promise { return new Promise((resolve) => { setTimeout(() => { @@ -31,45 +66,9 @@ export const userQueryRouter = router({ get: protectedProcedure .input(getUserInputSchema) .query(async function getUser({ ctx, input }) { - const apiResponse = await api.get(api.endpoints.v1.profile, { - cache: "no-store", - headers: { - Authorization: `Bearer ${ctx.session.token.access_token}`, - }, - }) + const verifiedData = await getVerifiedUser({ session: ctx.session }) - if (!apiResponse.ok) { - // switch (apiResponse.status) { - // case 400: - // throw badRequestError() - // case 401: - // throw unauthorizedError() - // case 403: - // throw forbiddenError() - // default: - // throw internalServerError() - // } - console.info(`API Response Failed - Getting User`) - console.info(`User: (${JSON.stringify(ctx.session.user)})`) - console.error(apiResponse) - return null - } - - const apiJson = await apiResponse.json() - if (!apiJson.data?.attributes) { - // throw notFound(apiJson) - console.error( - `User has no data - (user: ${JSON.stringify(ctx.session.user)})` - ) - return null - } - - const verifiedData = getUserSchema.safeParse(apiJson.data.attributes) - if (!verifiedData.success) { - console.info( - `Failed to validate User - (User: ${JSON.stringify(ctx.session.user)})` - ) - console.error(verifiedData.error) + if (!verifiedData) { return null } @@ -119,7 +118,33 @@ export const userQueryRouter = router({ return user }), + name: safeProtectedProcedure.query(async function ({ ctx }) { + if (!ctx.session) { + return null + } + const verifiedData = await getVerifiedUser({ session: ctx.session }) + if (!verifiedData) { + return null + } + return { + firstName: verifiedData.data.firstName, + lastName: verifiedData.data.lastName, + } + }), + membershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { + if (!ctx.session) { + return null + } + const verifiedData = await getVerifiedUser({ session: ctx.session }) + + if (!verifiedData) { + return null + } + + const membershipLevel = getMembership(verifiedData.data.memberships) + return membershipLevel + }), benefits: router({ current: protectedProcedure.query(async function (opts) { // TODO: Make request to get user data from Scandic API diff --git a/server/trpc.ts b/server/trpc.ts index 3f7cd3c70..1caf32b53 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -2,10 +2,16 @@ import { initTRPC } from "@trpc/server" import { env } from "@/env/server" -import { badRequestError, sessionExpiredError } from "./errors/trpc" +import { + badRequestError, + sessionExpiredError, + unauthorizedError, +} from "./errors/trpc" import { transformer } from "./transformer" import { langInput } from "./utils" +import type { Session } from "next-auth" + import type { Meta } from "@/types/trpc/meta" import type { Context } from "./context" @@ -57,6 +63,10 @@ export const protectedProcedure = t.procedure.use(async function (opts) { console.info(`path: ${opts.path} | type: ${opts.type}`) } + if (!session) { + throw unauthorizedError() + } + if (session?.error === "RefreshAccessTokenError") { throw sessionExpiredError() } @@ -67,3 +77,25 @@ export const protectedProcedure = t.procedure.use(async function (opts) { }, }) }) + +export const safeProtectedProcedure = t.procedure.use(async function (opts) { + const authRequired = opts.meta?.authRequired ?? true + + let session: Session | null = await opts.ctx.auth() + if (!authRequired && 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}`) + } + + if (!session || session.error === "RefreshAccessTokenError") { + session = null + } + + return opts.next({ + ctx: { + session, + }, + }) +}) diff --git a/types/components/current/header/mainMenu.ts b/types/components/current/header/mainMenu.ts index 4251fd529..8e468e8cc 100644 --- a/types/components/current/header/mainMenu.ts +++ b/types/components/current/header/mainMenu.ts @@ -16,6 +16,6 @@ export type MainMenuProps = { languageSwitcher: React.ReactNode | null myPagesMobileDropdown: React.ReactNode | null bookingHref: string - user: User | null + user: Pick | null lang: Lang } diff --git a/types/components/loyalty/blocks.ts b/types/components/loyalty/blocks.ts index f9f337344..c8db57354 100644 --- a/types/components/loyalty/blocks.ts +++ b/types/components/loyalty/blocks.ts @@ -7,9 +7,9 @@ import { RteBlockContent, } from "@/server/routers/contentstack/loyaltyPage/output" -import type { IntlFormatters } from "@formatjs/intl" +import { MembershipLevel } from "@/utils/user" -import { User } from "@/types/user" +import type { IntlFormatters } from "@formatjs/intl" export type BlocksProps = { blocks: Block[] @@ -32,7 +32,7 @@ export type Content = { content: RteBlockContent["content"]["content"] } type Benefit = { title: string } -export type OverviewTableProps = { user: User | null } +export type OverviewTableProps = { activeMembership: MembershipLevel | null } export type Level = { level: membershipLevels diff --git a/utils/user.ts b/utils/user.ts index 052826eaf..57f4e3e16 100644 --- a/utils/user.ts +++ b/utils/user.ts @@ -15,6 +15,7 @@ export function getMembership(memberships: User["memberships"]) { membership.membershipType.toLowerCase() === scandicMemberships.guestpr ) } +export type MembershipLevel = ReturnType export function getMembershipCards( memberships: z.infer