@@ -1,23 +1,34 @@
|
|||||||
"use client" // Error components must be Client Components
|
"use client" // Error components must be Client Components
|
||||||
|
|
||||||
import { usePathname } from "next/navigation"
|
import { useParams, usePathname } from "next/navigation"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
|
|
||||||
import { findLang } from "@/constants/languages"
|
import { findLang } from "@/constants/languages"
|
||||||
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
|
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
||||||
|
|
||||||
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
|
import { firaMono, firaSans } from "@/app/[lang]/(live)/fonts"
|
||||||
|
|
||||||
import styles from "./error.module.css"
|
import styles from "./error.module.css"
|
||||||
|
|
||||||
|
import { LangParams } from "@/types/params"
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
}: {
|
}: {
|
||||||
error: Error & { digest?: string }
|
error: Error & { digest?: string }
|
||||||
}) {
|
}) {
|
||||||
|
const params = useParams<LangParams>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}, [error])
|
|
||||||
|
if (error.message === SESSION_EXPIRED) {
|
||||||
|
const loginUrl = login[params.lang]
|
||||||
|
window.location.assign(loginUrl)
|
||||||
|
}
|
||||||
|
}, [error, params.lang])
|
||||||
|
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const lang = findLang(pathname)
|
const lang = findLang(pathname)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function RootLayout({
|
|||||||
<VwoScript />
|
<VwoScript />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<TrpcProvider>{children}</TrpcProvider>
|
<TrpcProvider lang={params.lang}>{children}</TrpcProvider>
|
||||||
<Script id="page-tracking">{`
|
<Script id="page-tracking">{`
|
||||||
typeof _satellite !== "undefined" && _satellite.pageBottom();
|
typeof _satellite !== "undefined" && _satellite.pageBottom();
|
||||||
`}</Script>
|
`}</Script>
|
||||||
|
|||||||
48
auth.ts
48
auth.ts
@@ -55,7 +55,8 @@ export const config = {
|
|||||||
async signIn() {
|
async signIn() {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
async session({ session, token, user }) {
|
async session({ session, token }) {
|
||||||
|
session.error = token.error
|
||||||
if (session.user) {
|
if (session.user) {
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
@@ -95,13 +96,54 @@ export const config = {
|
|||||||
async authorized({ auth, request }) {
|
async authorized({ auth, request }) {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
async jwt({ session, token, trigger, account }) {
|
async jwt({ account, session, token, trigger }) {
|
||||||
if (account) {
|
if (account) {
|
||||||
return {
|
return {
|
||||||
access_token: account.access_token,
|
access_token: account.access_token,
|
||||||
|
expires_at: account.expires_at
|
||||||
|
? account.expires_at * 1000
|
||||||
|
: undefined,
|
||||||
|
refresh_token: account.refresh_token,
|
||||||
|
}
|
||||||
|
} else if (Date.now() < token.expires_at) {
|
||||||
|
return token
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${env.CURITY_ISSUER_USER}/oauth/v2/token`,
|
||||||
|
{
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: env.CURITY_CLIENT_ID_USER,
|
||||||
|
client_secret: env.CURITY_CLIENT_SECRET_USER,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: token.refresh_token,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const tokens = await response.json()
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
access_token: tokens.access_token,
|
||||||
|
expires_at: tokens.expires_at,
|
||||||
|
refresh_token: tokens.refresh_token ?? token.refresh_token,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...token,
|
||||||
|
error: "RefreshAccessTokenError" as const,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return token
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// events: {
|
// events: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type { User } from "@/types/user"
|
|||||||
|
|
||||||
export default function CopyButton({ membership }: Pick<User, "membership">) {
|
export default function CopyButton({ membership }: Pick<User, "membership">) {
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
console.log(`COPIED! (${membership.membershipNumber})`)
|
console.log(`COPIED! (${membership ? membership.membershipNumber : "N/A"})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function Friend({ user }: FriendProps) {
|
|||||||
<h3 className={styles.name}>{user.name}</h3>
|
<h3 className={styles.name}>{user.name}</h3>
|
||||||
<div className={styles.membershipContainer}>
|
<div className={styles.membershipContainer}>
|
||||||
<p className={styles.membershipId}>
|
<p className={styles.membershipId}>
|
||||||
{user.membership.membershipNumber}
|
{user.membership ? user.membership.membershipNumber : "N/A"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ export default function TotalPoints({ user }: TotalPointsProps) {
|
|||||||
<div>
|
<div>
|
||||||
<Title>Total Points</Title>
|
<Title>Total Points</Title>
|
||||||
<Divider className={styles.divider} variant="dotted" />
|
<Divider className={styles.divider} variant="dotted" />
|
||||||
<p className={styles.points}>{user.membership.currentPoints}</p>
|
<p className={styles.points}>
|
||||||
|
{user.membership ? user.membership.currentPoints : "N/A"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export namespace endpoints {
|
|||||||
profile = "profile/v0/Profile",
|
profile = "profile/v0/Profile",
|
||||||
}
|
}
|
||||||
export const enum v1 {
|
export const enum v1 {
|
||||||
|
profile = "profile/v1/Profile",
|
||||||
upcomingStays = "booking/v1/Stays/future",
|
upcomingStays = "booking/v1/Stays/future",
|
||||||
previousStays = "booking/v1/Stays/past",
|
previousStays = "booking/v1/Stays/past",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
#import "../Fragments/Refs/System.graphql"
|
#import "../Fragments/Refs/System.graphql"
|
||||||
|
|
||||||
query ResolveEntryByUrl($locale: String!, $url: String!) {
|
query ResolveEntryByUrl($locale: String!, $url: String!) {
|
||||||
|
all_account_page(where: { url: $url }, locale: $locale) {
|
||||||
|
items {
|
||||||
|
system {
|
||||||
|
...System
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
all_content_page(where: { url: $url }, locale: $locale) {
|
all_content_page(where: { url: $url }, locale: $locale) {
|
||||||
items {
|
items {
|
||||||
system {
|
system {
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
import {
|
||||||
import { httpBatchLink, loggerLink } from "@trpc/client"
|
QueryCache,
|
||||||
|
QueryClient,
|
||||||
|
QueryClientProvider,
|
||||||
|
} from "@tanstack/react-query"
|
||||||
|
import { httpBatchLink, loggerLink, TRPCClientError } from "@trpc/client"
|
||||||
|
import { AnyTRPCRouter } from "@trpc/server"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { login } from "@/constants/routes/handleAuth"
|
||||||
import { env } from "@/env/client"
|
import { env } from "@/env/client"
|
||||||
|
import { SessionExpiredError } from "@/server/errors/trpc"
|
||||||
import { transformer } from "@/server/transformer"
|
import { transformer } from "@/server/transformer"
|
||||||
|
|
||||||
import { trpc } from "./client"
|
import { trpc } from "./client"
|
||||||
|
|
||||||
|
import { LangParams } from "@/types/params"
|
||||||
|
|
||||||
function initializeTrpcClient() {
|
function initializeTrpcClient() {
|
||||||
// Locally we set nextjs to run on port to 3000 so that we always guarantee
|
// Locally we set nextjs to run on port to 3000 so that we always guarantee
|
||||||
// that trpc and next are running on the same port.
|
// that trpc and next are running on the same port.
|
||||||
@@ -32,8 +41,58 @@ function initializeTrpcClient() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TrpcProvider({ children }: React.PropsWithChildren) {
|
export default function TrpcProvider({
|
||||||
const [queryClient] = useState(() => new QueryClient({}))
|
children,
|
||||||
|
lang,
|
||||||
|
}: React.PropsWithChildren<LangParams>) {
|
||||||
|
const [queryClient] = useState(
|
||||||
|
() =>
|
||||||
|
new QueryClient({
|
||||||
|
queryCache: new QueryCache({
|
||||||
|
async onError(error) {
|
||||||
|
if (error instanceof TRPCClientError) {
|
||||||
|
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||||
|
console.log({ appError })
|
||||||
|
if (appError.data?.code === "UNAUTHORIZED") {
|
||||||
|
if (appError.data?.cause instanceof SessionExpiredError) {
|
||||||
|
const loginUrl = login[lang]
|
||||||
|
window.location.assign(loginUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
staleTime: 3000,
|
||||||
|
retry(failureCount, error) {
|
||||||
|
if (error instanceof TRPCClientError) {
|
||||||
|
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||||
|
|
||||||
|
// Do not retry query requests that got UNAUTHORIZED error.
|
||||||
|
// It won't make a difference sending the same request again.
|
||||||
|
|
||||||
|
if (appError.data?.code) {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
"UNAUTHORIZED",
|
||||||
|
"INTERNAL_SERVER_ERROR",
|
||||||
|
"FORBIDDEN",
|
||||||
|
].includes(appError.data.code)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry all client requests that fail (and are not handled above)
|
||||||
|
// at most 3 times.
|
||||||
|
return failureCount < 3
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
const [trpcClient] = useState(() => initializeTrpcClient())
|
const [trpcClient] = useState(() => initializeTrpcClient())
|
||||||
return (
|
return (
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function serverClient() {
|
|||||||
if (error.code === "UNAUTHORIZED") {
|
if (error.code === "UNAUTHORIZED") {
|
||||||
const lang = ctx?.lang || Lang.en
|
const lang = ctx?.lang || Lang.en
|
||||||
const pathname = ctx?.pathname || "/"
|
const pathname = ctx?.pathname || "/"
|
||||||
redirect(`/${lang}/login?redirectTo=${encodeURIComponent(pathname)}`)
|
redirect(
|
||||||
|
`/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,20 @@ export const middleware: NextMiddleware = async (request, event) => {
|
|||||||
|
|
||||||
// We use x-lang as either Akamai or Netlify use x-language as the users preferred language
|
// We use x-lang as either Akamai or Netlify use x-language as the users preferred language
|
||||||
result?.headers.set("x-lang", lang)
|
result?.headers.set("x-lang", lang)
|
||||||
result?.headers.set("x-pathname", request.nextUrl.pathname)
|
result?.headers.set(
|
||||||
|
"x-pathname",
|
||||||
|
request.nextUrl.pathname.replace(`/${lang}`, "")
|
||||||
|
)
|
||||||
result?.headers.set("x-url", request.nextUrl.href)
|
result?.headers.set("x-url", request.nextUrl.href)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof NextResponse && e.status) {
|
if (e instanceof NextResponse && e.status) {
|
||||||
|
const cause = await e.json()
|
||||||
|
console.error(`Error in middleware`)
|
||||||
|
console.error(cause)
|
||||||
|
|
||||||
return NextResponse.rewrite(
|
return NextResponse.rewrite(
|
||||||
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
|
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ export const middleware = auth(async (request) => {
|
|||||||
const lang = findLang(nextUrl.pathname)!
|
const lang = findLang(nextUrl.pathname)!
|
||||||
|
|
||||||
const isLoggedIn = !!request.auth
|
const isLoggedIn = !!request.auth
|
||||||
|
const hasError = request.auth?.error
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn && !hasError) {
|
||||||
const headers = new Headers(request.headers)
|
const headers = new Headers(request.headers)
|
||||||
headers.set("x-continue", "1")
|
headers.set("x-continue", "1")
|
||||||
return NextResponse.next({
|
return NextResponse.next({
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
const { contentType, uid } = await resolveEntry(pathNameWithoutLang, lang)
|
const { contentType, uid } = await resolveEntry(pathNameWithoutLang, lang)
|
||||||
|
|
||||||
if (!contentType || !uid) {
|
if (!contentType || !uid) {
|
||||||
throw notFound()
|
throw notFound(
|
||||||
|
`Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCurrent = contentType ? contentType.indexOf("current") >= 0 : false
|
const isCurrent = contentType ? contentType.indexOf("current") >= 0 : false
|
||||||
|
|||||||
@@ -2,23 +2,53 @@ import { NextResponse } from "next/server"
|
|||||||
|
|
||||||
import { findLang } from "@/constants/languages"
|
import { findLang } from "@/constants/languages"
|
||||||
import { myPages, overview } from "@/constants/routes/myPages"
|
import { myPages, overview } from "@/constants/routes/myPages"
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
import { internalServerError, notFound } from "@/server/errors/next"
|
||||||
|
|
||||||
|
import { resolve as resolveEntry } from "@/utils/entry"
|
||||||
|
|
||||||
import type { NextMiddleware } from "next/server"
|
import type { NextMiddleware } from "next/server"
|
||||||
|
|
||||||
import type { MiddlewareMatcher } from "@/types/middleware"
|
import type { MiddlewareMatcher } from "@/types/middleware"
|
||||||
|
|
||||||
export const middleware: NextMiddleware = (request) => {
|
export const middleware: NextMiddleware = async (request) => {
|
||||||
const lang = findLang(request.nextUrl.pathname)!
|
const { nextUrl } = request
|
||||||
return NextResponse.redirect(overview[lang])
|
const lang = findLang(nextUrl.pathname)!
|
||||||
|
|
||||||
|
const myPagesRoot = myPages[lang]
|
||||||
|
if (nextUrl.pathname === myPagesRoot) {
|
||||||
|
if (!env.PUBLIC_URL) {
|
||||||
|
throw internalServerError("Missing value for env.PUBLIC_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicUrl = new URL(env.PUBLIC_URL)
|
||||||
|
const nextUrlClone = nextUrl.clone()
|
||||||
|
nextUrlClone.host = publicUrl.host
|
||||||
|
nextUrlClone.hostname = publicUrl.hostname
|
||||||
|
|
||||||
|
const overviewUrl = overview[lang]
|
||||||
|
return NextResponse.redirect(new URL(overviewUrl, nextUrlClone))
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "")
|
||||||
|
const { uid } = await resolveEntry(pathNameWithoutLang, lang)
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
throw notFound(
|
||||||
|
`Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = new Headers(request.headers)
|
||||||
|
headers.set("x-uid", uid)
|
||||||
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const matcher: MiddlewareMatcher = (request) => {
|
export const matcher: MiddlewareMatcher = (request) => {
|
||||||
return [
|
const lang = findLang(request.nextUrl.pathname)!
|
||||||
myPages.da,
|
return request.nextUrl.pathname.startsWith(myPages[lang])
|
||||||
myPages.de,
|
|
||||||
myPages.en,
|
|
||||||
myPages.fi,
|
|
||||||
myPages.no,
|
|
||||||
myPages.sv,
|
|
||||||
].includes(request.nextUrl.pathname)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,43 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
export function badRequest(body: unknown | string = "Bad request") {
|
export function badRequest(cause?: unknown) {
|
||||||
const resInit = {
|
const resInit = {
|
||||||
status: 400,
|
status: 400,
|
||||||
statusText: "Bad request",
|
statusText: "Bad request",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body === "string") {
|
return NextResponse.json(
|
||||||
return new NextResponse(body, resInit)
|
{
|
||||||
}
|
cause,
|
||||||
|
},
|
||||||
return NextResponse.json(body, resInit)
|
resInit
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notFound(body: unknown | string = "Not found") {
|
export function notFound(cause?: unknown) {
|
||||||
const resInit = {
|
const resInit = {
|
||||||
status: 404,
|
status: 404,
|
||||||
statusText: "Not found",
|
statusText: "Not found",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body === "string") {
|
return NextResponse.json(
|
||||||
return new NextResponse(body, resInit)
|
{
|
||||||
}
|
cause,
|
||||||
|
},
|
||||||
return NextResponse.json(body, resInit)
|
resInit
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function internalServerError(
|
export function internalServerError(cause?: unknown) {
|
||||||
body: unknown | string = "Internal Server Error"
|
|
||||||
) {
|
|
||||||
const resInit = {
|
const resInit = {
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: "Internal Server Error",
|
statusText: "Internal Server Error",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body === "string") {
|
return NextResponse.json(
|
||||||
return new NextResponse(body, resInit)
|
{
|
||||||
}
|
cause,
|
||||||
|
},
|
||||||
return NextResponse.json(body, resInit)
|
resInit
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,3 +39,13 @@ export function internalServerError(cause?: unknown) {
|
|||||||
cause,
|
cause,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SESSION_EXPIRED = "SESSION_EXPIRED"
|
||||||
|
export class SessionExpiredError extends Error {}
|
||||||
|
export function sessionExpiredError() {
|
||||||
|
return new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: SESSION_EXPIRED,
|
||||||
|
cause: new SessionExpiredError(SESSION_EXPIRED),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export const getUserSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
language: z.string(),
|
language: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
membership: z.object({
|
membership: z
|
||||||
currentPoints: z.number(),
|
.object({
|
||||||
expirationDate: z.string(),
|
currentPoints: z.number(),
|
||||||
membershipNumber: z.string(),
|
expirationDate: z.string(),
|
||||||
memberSince: z.string(),
|
membershipNumber: z.string(),
|
||||||
}),
|
memberSince: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
phoneNumber: z.string(),
|
phoneNumber: z.string(),
|
||||||
profileId: z.string(),
|
profileId: z.string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function fakingRequest<T>(payload: T): Promise<T> {
|
|||||||
export const userQueryRouter = router({
|
export const userQueryRouter = router({
|
||||||
get: protectedProcedure.query(async function ({ ctx }) {
|
get: protectedProcedure.query(async function ({ ctx }) {
|
||||||
const apiResponse = await api.get(api.endpoints.v0.profile, {
|
const apiResponse = await api.get(api.endpoints.v0.profile, {
|
||||||
|
cache: "no-store",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||||
},
|
},
|
||||||
@@ -164,10 +165,6 @@ export const userQueryRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiJson = await apiResponse.json()
|
const apiJson = await apiResponse.json()
|
||||||
if (!apiJson.data?.length) {
|
|
||||||
throw notFound(apiJson)
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||||
if (!verifiedData.success) {
|
if (!verifiedData.success) {
|
||||||
throw internalServerError(verifiedData.error)
|
throw internalServerError(verifiedData.error)
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { initTRPC } from "@trpc/server"
|
|||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import { badRequestError, unauthorizedError } from "./errors/trpc"
|
import {
|
||||||
|
badRequestError,
|
||||||
|
sessionExpiredError,
|
||||||
|
unauthorizedError,
|
||||||
|
} from "./errors/trpc"
|
||||||
import { transformer } from "./transformer"
|
import { transformer } from "./transformer"
|
||||||
|
|
||||||
import type { Meta } from "@/types/trpc/meta"
|
import type { Meta } from "@/types/trpc/meta"
|
||||||
@@ -34,6 +38,10 @@ export const protectedProcedure = t.procedure.use(async function (opts) {
|
|||||||
console.info(`path: ${opts.path} | type: ${opts.type}`)
|
console.info(`path: ${opts.path} | type: ${opts.type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session?.error === "RefreshAccessTokenError") {
|
||||||
|
throw sessionExpiredError()
|
||||||
|
}
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
throw unauthorizedError()
|
throw unauthorizedError()
|
||||||
}
|
}
|
||||||
|
|||||||
27
types/auth.d.ts
vendored
27
types/auth.d.ts
vendored
@@ -1,8 +1,23 @@
|
|||||||
import type { JWT } from "next-auth/jwt"
|
import type { JWT } from "next-auth/jwt"
|
||||||
|
|
||||||
|
import type { RefreshTokenError } from "./authError"
|
||||||
|
|
||||||
// Module augmentation
|
// Module augmentation
|
||||||
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
||||||
declare module "next-auth" {
|
declare module "next-auth" {
|
||||||
|
/**
|
||||||
|
* The shape of the account object returned in the OAuth providers' `account` callback,
|
||||||
|
* Usually contains information about the provider being used, like OAuth tokens (`access_token`, etc).
|
||||||
|
*/
|
||||||
|
interface Account {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returned by `useSession`, `auth`, contains information about the active session.
|
||||||
|
*/
|
||||||
|
interface Session extends RefreshTokenError {
|
||||||
|
token: JWT
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shape of the user object returned in the OAuth providers' `profile` callback,
|
* The shape of the user object returned in the OAuth providers' `profile` callback,
|
||||||
* or the second parameter of the `session` callback, when using a database.
|
* or the second parameter of the `session` callback, when using a database.
|
||||||
@@ -11,16 +26,4 @@ declare module "next-auth" {
|
|||||||
given_name: string
|
given_name: string
|
||||||
sub: string
|
sub: string
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* The shape of the account object returned in the OAuth providers' `account` callback,
|
|
||||||
* Usually contains information about the provider being used, like OAuth tokens (`access_token`, etc).
|
|
||||||
*/
|
|
||||||
interface Account { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returned by `useSession`, `auth`, contains information about the active session.
|
|
||||||
*/
|
|
||||||
interface Session {
|
|
||||||
token: JWT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3
types/authError.ts
Normal file
3
types/authError.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface RefreshTokenError {
|
||||||
|
error?: "RefreshAccessTokenError"
|
||||||
|
}
|
||||||
8
types/jwt.d.ts
vendored
8
types/jwt.d.ts
vendored
@@ -1,8 +1,14 @@
|
|||||||
|
import type { DefaultJWT } from "next-auth/jwt"
|
||||||
|
|
||||||
|
import type { RefreshTokenError } from "./authError"
|
||||||
|
|
||||||
// Module augmentation
|
// Module augmentation
|
||||||
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
// https://authjs.dev/getting-started/typescript#popular-interfaces-to-augment
|
||||||
declare module "next-auth/jwt" {
|
declare module "next-auth/jwt" {
|
||||||
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
|
/** Returned by the `jwt` callback and `auth`, when using JWT sessions */
|
||||||
interface JWT {
|
interface JWT extends DefaultJWT, RefreshTokenError {
|
||||||
access_token: string
|
access_token: string
|
||||||
|
expires_at: number
|
||||||
|
refresh_token: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const entryResolveSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const validateEntryResolveSchema = z.object({
|
export const validateEntryResolveSchema = z.object({
|
||||||
|
all_account_page: entryResolveSchema,
|
||||||
all_content_page: entryResolveSchema,
|
all_content_page: entryResolveSchema,
|
||||||
all_loyalty_page: entryResolveSchema,
|
all_loyalty_page: entryResolveSchema,
|
||||||
all_current_blocks_page: entryResolveSchema,
|
all_current_blocks_page: entryResolveSchema,
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ export async function resolve(url: string, lang = Lang.en) {
|
|||||||
const validatedData = validateEntryResolveSchema.safeParse(data)
|
const validatedData = validateEntryResolveSchema.safeParse(data)
|
||||||
|
|
||||||
if (!validatedData.success) {
|
if (!validatedData.success) {
|
||||||
console.error("Bad validation for `validateContentTypeDataSchema`")
|
throw internalServerError(validatedData.error)
|
||||||
console.error(validatedData.error)
|
|
||||||
throw internalServerError()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const value of Object.values(validatedData.data)) {
|
for (const value of Object.values(validatedData.data)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user