diff --git a/app/[lang]/(live)/(protected)/layout.tsx b/app/[lang]/(live)/(protected)/layout.tsx index 024071af3..19ad58daf 100644 --- a/app/[lang]/(live)/(protected)/layout.tsx +++ b/app/[lang]/(live)/(protected)/layout.tsx @@ -20,13 +20,17 @@ export default async function ProtectedLayout({ h.get("x-url") ?? h.get("x-pathname") ?? overview[getLang()] ) + const redirectURL = `/${getLang()}/login?redirectTo=${redirectTo}` + if (!session) { - redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) + console.log(`[layout:protected] no session, redirecting to: ${redirectURL}`) + redirect(redirectURL) } const user = await serverClient().user.get() if (!user || "error" in user) { - redirect(`/${getLang()}/login?redirectTo=${redirectTo}`) + console.log(`[layout:protected] no user, redirecting to: ${redirectURL}`) + redirect(redirectURL) } return children diff --git a/app/[lang]/(live)/(protected)/logout/route.ts b/app/[lang]/(live)/(protected)/logout/route.ts index 38b98361a..2bb2f0a52 100644 --- a/app/[lang]/(live)/(protected)/logout/route.ts +++ b/app/[lang]/(live)/(protected)/logout/route.ts @@ -1,5 +1,3 @@ -import { createActionURL } from "@auth/core" -import { headers as nextHeaders } from "next/headers" import { NextRequest, NextResponse } from "next/server" import { AuthError } from "next-auth" @@ -16,11 +14,35 @@ export async function GET( let redirectTo: string = "" const returnUrl = request.headers.get("x-returnurl") + const isSeamless = request.headers.get("x-logout-source") === "seamless" - if (returnUrl) { - // Seamless logout request from Current web - redirectTo = returnUrl + console.log( + `[logout] source: ${request.headers.get("x-logout-source") || "normal"}` + ) + + const redirectToSearchParamValue = + request.nextUrl.searchParams.get("redirectTo") + const redirectToFallback = "/" + + if (isSeamless) { + if (returnUrl) { + redirectTo = returnUrl + } else { + console.log( + `[login] missing returnUrl, using fallback: ${redirectToFallback}` + ) + redirectTo = redirectToFallback + } } else { + redirectTo = redirectToSearchParamValue || redirectToFallback + + // Make relative URL to absolute URL + if (redirectTo.startsWith("/")) { + console.log(`[logout] make redirectTo absolute, from ${redirectTo}`) + redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + console.log(`[logout] make redirectTo absolute, to ${redirectTo}`) + } + try { // Initiate the seamless logout flow let redirectUrlValue @@ -45,6 +67,9 @@ export async function GET( break } const redirectUrl = new URL(redirectUrlValue) + console.log( + `[logout] creating redirect to seamless logout: ${redirectUrl}` + ) redirectTo = redirectUrl.toString() } catch (e) { console.error( @@ -55,37 +80,25 @@ export async function GET( } try { + redirectTo = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}` + console.log(`[logout] final redirectUrl: ${redirectTo}`) + console.log({ logout_env: process.env }) + /** * Passing `redirect: false` to `signOut` will return a result object * instead of automatically redirecting inside of `signOut`. * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L104 */ - console.log({ logout_NEXTAUTH_URL: process.env.NEXTAUTH_URL }) - console.log({ logout_env: process.env }) - - const headers = new Headers(nextHeaders()) - const signOutURL = createActionURL( - "signout", - // @ts-expect-error `x-forwarded-proto` is not nullable, next.js sets it by default - headers.get("x-forwarded-proto"), - headers, - process.env - ) - - console.log({ logout_signOutURL: signOutURL }) - - // Redirect to Curity logout - const curityLogoutUrl = `${env.CURITY_ISSUER_USER}/authn/authenticate/logout?redirect_uri=${encodeURIComponent(redirectTo)}` - - console.log({ logout_redirectTo: curityLogoutUrl }) - const redirectUrlObj = await signOut({ - redirectTo: curityLogoutUrl, + redirectTo, redirect: false, }) if (redirectUrlObj) { + console.log(`[logout] redirecting to: ${redirectUrlObj.redirect}`) return NextResponse.redirect(redirectUrlObj.redirect) + } else { + console.error(`[logout] missing redirectUrlObj reponse from signOut()`) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/(live)/(public)/login/route.ts b/app/[lang]/(live)/(public)/login/route.ts index e5a7108fd..702282d22 100644 --- a/app/[lang]/(live)/(public)/login/route.ts +++ b/app/[lang]/(live)/(public)/login/route.ts @@ -11,6 +11,10 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { + if (!env.PUBLIC_URL) { + throw internalServerError("No value for env.PUBLIC_URL") + } + let redirectHeaders: Headers | undefined = undefined let redirectTo: string @@ -20,10 +24,6 @@ export async function GET( const isSeamlessMagicLink = request.headers.get("x-login-source") === "seamless-magiclink" - if (!env.PUBLIC_URL) { - throw internalServerError("No value for env.PUBLIC_URL") - } - console.log( `[login] source: ${request.headers.get("x-login-source") || "normal"}` ) @@ -32,6 +32,7 @@ export async function GET( const redirectToSearchParamValue = request.nextUrl.searchParams.get("redirectTo") const redirectToFallback = "/" + console.log(`[login] redirectTo cookie value: ${redirectToCookieValue}`) console.log( `[login] redirectTo search param value: ${redirectToSearchParamValue}` @@ -104,20 +105,16 @@ export async function GET( ) } catch (e) { console.error( - "Unable to create URL for seamless login, proceeding without it." + "[login] unable to create URL for seamless login, proceeding without it.", + e ) - console.error(e) } } try { - /** - * Passing `redirect: false` to `signIn` will return the URL instead of - * automatically redirecting to it inside of `signIn`. - * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 - */ console.log(`[login] final redirectUrl: ${redirectTo}`) console.log({ login_env: process.env }) + /** Record is next-auth typings */ const params: Record = { ui_locales: context.params.lang, @@ -152,6 +149,11 @@ export async function GET( params.acr_values = "abc" } params.scope = params.scope.join(" ") + /** + * Passing `redirect: false` to `signIn` will return the URL instead of + * automatically redirecting to it inside of `signIn`. + * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 + */ const redirectUrl = await signIn( "curity", { @@ -162,9 +164,13 @@ export async function GET( ) if (redirectUrl) { - return NextResponse.redirect(redirectUrl, { + const redirectOpts = { headers: redirectHeaders, - }) + } + console.log(`[login] redirecting to: ${redirectUrl}`, redirectOpts) + return NextResponse.redirect(redirectUrl, redirectOpts) + } else { + console.error(`[login] missing redirectUrl reponse from signIn()`) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/(live)/(public)/verifymagiclink/route.ts b/app/[lang]/(live)/(public)/verifymagiclink/route.ts index 4f2a1e86d..12c4b132f 100644 --- a/app/[lang]/(live)/(public)/verifymagiclink/route.ts +++ b/app/[lang]/(live)/(public)/verifymagiclink/route.ts @@ -12,39 +12,54 @@ export async function GET( request: NextRequest, context: { params: { lang: Lang } } ) { - let redirectTo: string - - // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login - redirectTo = - request.cookies.get("magicLinkRedirectTo")?.value || - "/" + context.params.lang - if (!env.PUBLIC_URL) { throw internalServerError("No value for env.PUBLIC_URL") } + const loginKey = request.nextUrl.searchParams.get("loginKey") + if (!loginKey) { + console.log( + `[verifymagiclink] missing required loginKey, aborting bad request` + ) + return badRequest() + } + + let redirectTo: string + + console.log(`[verifymagiclink] verifying callback`) + + const redirectToCookieValue = request.cookies.get( + "magicLinkRedirectTo" + )?.value // Set redirect url from the magicLinkRedirect Cookie which is set when intiating login + const redirectToFallback = "/" + + console.log( + `[verifymagiclink] magicLinkRedirectTo cookie value: ${redirectToCookieValue}` + ) + + redirectTo = redirectToCookieValue || redirectToFallback + // Make relative URL to absolute URL if (redirectTo.startsWith("/")) { + console.log( + `[verifymagiclink] make redirectTo absolute, from ${redirectTo}` + ) redirectTo = new URL(redirectTo, env.PUBLIC_URL).href + console.log(`[verifymagiclink] make redirectTo absolute, to ${redirectTo}`) } // Update Seamless login url as Magic link login has a different authenticator in Curity redirectTo = redirectTo.replace("updatelogin", "updateloginemail") - const loginKey = request.nextUrl.searchParams.get("loginKey") - - if (!loginKey) { - return badRequest() - } - try { + console.log(`[verifymagiclink] final redirectUrl: ${redirectTo}`) + /** * Passing `redirect: false` to `signIn` will return the URL instead of * automatically redirecting to it inside of `signIn`. * https://github.com/nextauthjs/next-auth/blob/3c035ec/packages/next-auth/src/lib/actions.ts#L76 */ - console.log({ login_redirectTo: redirectTo }) - let redirectUrl = await signIn( + const redirectUrl = await signIn( "curity", { redirectTo, @@ -61,7 +76,12 @@ export async function GET( ) if (redirectUrl) { + console.log(`[verifymagiclink] redirecting to: ${redirectUrl}`) return NextResponse.redirect(redirectUrl) + } else { + console.error( + `[verifymagiclink] missing redirectUrl reponse from signIn()` + ) } } catch (error) { if (error instanceof AuthError) { diff --git a/app/[lang]/webview/[contentType]/[uid]/page.tsx b/app/[lang]/webview/[contentType]/[uid]/page.tsx index 0efdea462..88bfd57e3 100644 --- a/app/[lang]/webview/[contentType]/[uid]/page.tsx +++ b/app/[lang]/webview/[contentType]/[uid]/page.tsx @@ -21,6 +21,7 @@ export default async function ContentTypePage({ const user = await serverClient().user.get() if (!user) { + console.log(`[webview:page] unable to load user`) return

Error: No user could be loaded

} @@ -31,9 +32,16 @@ export default async function ContentTypePage({ case "token_expired": const h = headers() const returnURL = `/${getLang()}/webview${h.get("x-pathname")!}` - redirect( - `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` - ) + const redirectURL = `/${getLang()}/webview/refresh?returnUrl=${encodeURIComponent(returnURL)}` + console.log(`[webview:page] user error, redirecting to: ${redirectURL}`) + redirect(redirectURL) + case "notfound": + return

Error: user not found

+ case "unknown": + return

Unknown error occurred loading user

+ default: + const u: never = user + console.log(`[webview:page] unhandled user loading error`) } } diff --git a/app/api/web/add-card-callback/[lang]/route.ts b/app/api/web/add-card-callback/[lang]/route.ts index 871b5dd14..63eb19942 100644 --- a/app/api/web/add-card-callback/[lang]/route.ts +++ b/app/api/web/add-card-callback/[lang]/route.ts @@ -9,6 +9,7 @@ export async function GET( request: NextRequest, { params }: { params: { lang: string } } ) { + console.log(`[add-card] callback started`) const lang = params.lang as Lang const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`) @@ -26,22 +27,28 @@ export async function GET( }) if (saveCardSuccess) { + console.log(`[add-card] planet success: card saved success`) returnUrl.searchParams.set("success", "true") } else { + console.log(`[add-card] planet success: card saved fail`) returnUrl.searchParams.set("failure", "true") } } else { + console.log(`[add-card] planet success: missing datatransTrxId`) returnUrl.searchParams.set("error", "true") } } else if (failure) { + console.log(`[add-card] planet fail`) returnUrl.searchParams.set("failure", "true") } else if (cancel) { + console.log(`[add-card] planet cancel`) returnUrl.searchParams.set("cancel", "true") } - } catch (error) { - console.error("Error saving credit card", error) + } catch (e) { + console.error(`[add-card] error saving credit card`, e) returnUrl.searchParams.set("error", "true") } - return Response.redirect(returnUrl, 307) + console.log(`[add-card] redirecting to: ${returnUrl}`) + return Response.redirect(returnUrl) } diff --git a/auth.ts b/auth.ts index 5dbf1097e..29d470f4d 100644 --- a/auth.ts +++ b/auth.ts @@ -136,7 +136,9 @@ export const config = { return session }, async redirect({ baseUrl, url }) { + console.log(`[auth] deciding redirect URL`, { baseUrl, url }) if (url.startsWith("/")) { + console.log(`[auth] relative URL accepted, returning: ${baseUrl}${url}`) // Allows relative callback URLs return `${baseUrl}${url}` } else { @@ -146,17 +148,19 @@ export const config = { if ( /\.scandichotels\.(dk|de|com|fi|no|se)$/.test(parsedUrl.hostname) ) { + console.log(`[auth] subdomain URL accepted, returning: ${url}`) // Allows any subdomains on all top level domains above return url } else if (parsedUrl.origin === baseUrl) { // Allows callback URLs on the same origin + console.log(`[auth] origin URL accepted, returning: ${url}`) return url } } catch (e) { - console.error("Error in auth redirect callback") - console.error(e) + console.error(`[auth] error parsing incoming URL for redirection`, e) } } + console.log(`[auth] URL denied, returning base URL: ${baseUrl}`) return baseUrl }, async authorized({ auth, request }) { diff --git a/components/Current/Header/LoginButton.tsx b/components/Current/Header/LoginButton.tsx index 9e8144641..8634b71bc 100644 --- a/components/Current/Header/LoginButton.tsx +++ b/components/Current/Header/LoginButton.tsx @@ -44,6 +44,7 @@ export default function LoginButton({ id={trackingId} color={color} href={`${login[lang]}?redirectTo=${encodeURIComponent(pathName)}`} + prefetch={false} > {children} diff --git a/lib/graphql/_request.ts b/lib/graphql/_request.ts index 9dad21077..b3253f26b 100644 --- a/lib/graphql/_request.ts +++ b/lib/graphql/_request.ts @@ -50,9 +50,9 @@ export async function request( const nr = Math.random() console.log(`START REQUEST ${nr}`) console.time(`OUTGOING REQUEST ${nr}`) - console.log(`Sending reqeust to ${env.CMS_URL}`) - console.log(`Query:`, print(query as DocumentNode)) - console.log(`Variables:`, variables) + // console.log(`Sending reqeust to ${env.CMS_URL}`) + // console.log(`Query:`, print(query as DocumentNode)) + // console.log(`Variables:`, variables) const response = await client.request({ document: query, @@ -64,7 +64,7 @@ export async function request( }) console.timeEnd(`OUTGOING REQUEST ${nr}`) - console.log({ response }) + // console.log({ response }) return { data: response } } catch (error) { diff --git a/lib/trpc/server.ts b/lib/trpc/server.ts index d824755b4..383da4966 100644 --- a/lib/trpc/server.ts +++ b/lib/trpc/server.ts @@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server" import { redirect } from "next/navigation" import { Lang } from "@/constants/languages" +import { login } from "@/constants/routes/handleAuth" import { webviews } from "@/constants/routes/webviews" import { appRouter } from "@/server" import { createContext } from "@/server/context" @@ -13,12 +14,11 @@ const createCaller = createCallerFactory(appRouter) export function serverClient() { return createCaller(createContext(), { onError: ({ ctx, error, input, path, type }) => { - console.error(`Server Client error for ${type}: ${path}`) + console.error(`[serverClient] error for ${type}: ${path}`, error) + if (input) { - console.error(`Received input:`) - console.error(input) + console.error(`[serverClient] received input:`, input) } - console.error(error) if (error instanceof TRPCError) { if (error.code === "UNAUTHORIZED") { @@ -41,12 +41,13 @@ export function serverClient() { redirectUrl ) + console.log(`[serverClient] onError redirecting to: ${redirectUrl}`) redirect(redirectUrl) } - redirect( - `/${lang}/login?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` - ) + const redirectUrl = `${login[lang]}?redirectTo=${encodeURIComponent(`/${lang}/${pathname}`)}` + console.log(`[serverClient] onError redirecting to: ${redirectUrl}`) + redirect(redirectUrl) } } diff --git a/middlewares/authRequired.ts b/middlewares/authRequired.ts index 896bbfd3a..0e50330ee 100644 --- a/middlewares/authRequired.ts +++ b/middlewares/authRequired.ts @@ -49,9 +49,9 @@ export const middleware = auth(async (request) => { } const publicUrl = new URL(env.PUBLIC_URL) - const nextUrlClone = nextUrl.clone() - nextUrlClone.host = publicUrl.host - nextUrlClone.hostname = publicUrl.hostname + const nextUrlPublic = nextUrl.clone() + nextUrlPublic.host = publicUrl.host + nextUrlPublic.hostname = publicUrl.hostname /** * Function to validate MFA from token data @@ -67,7 +67,7 @@ export const middleware = auth(async (request) => { if (isLoggedIn && isMFAPath && isMFAInvalid()) { const headers = new Headers(request.headers) - headers.set("x-returnurl", nextUrlClone.href) + headers.set("x-returnurl", nextUrlPublic.href) headers.set("x-login-source", "mfa") return NextResponse.rewrite(new URL(`/${lang}/login`, request.nextUrl), { request: { @@ -87,13 +87,16 @@ export const middleware = auth(async (request) => { const headers = new Headers() headers.append( "set-cookie", - `redirectTo=${encodeURIComponent(nextUrlClone.href)}; Path=/; HttpOnly; SameSite=Lax` + `redirectTo=${encodeURIComponent(nextUrlPublic.href)}; Path=/; HttpOnly; SameSite=Lax` ) const loginUrl = login[lang] - return NextResponse.redirect(new URL(loginUrl, nextUrlClone), { + const redirectUrl = new URL(loginUrl, nextUrlPublic) + const redirectOpts = { headers, - }) + } + console.log(`[authRequired] redirecting to: ${redirectUrl}`, redirectOpts) + return NextResponse.redirect(redirectUrl, redirectOpts) }) as NextMiddleware // See comment above export const matcher: MiddlewareMatcher = (request) => { diff --git a/middlewares/currentWebLogout.ts b/middlewares/currentWebLogout.ts index 43fba7669..c358012f2 100644 --- a/middlewares/currentWebLogout.ts +++ b/middlewares/currentWebLogout.ts @@ -23,6 +23,7 @@ export const middleware: NextMiddleware = (request) => { const headers = new Headers(request.headers) headers.set("x-returnurl", redirectTo) + headers.set("x-logout-source", "seamless") return NextResponse.rewrite(new URL(`/${lang}/logout`, request.nextUrl), { request: { diff --git a/middlewares/myPages.ts b/middlewares/myPages.ts index bf405209c..fcadc8736 100644 --- a/middlewares/myPages.ts +++ b/middlewares/myPages.ts @@ -34,7 +34,9 @@ export const middleware: NextMiddleware = async (request) => { nextUrlClone.hostname = publicUrl.hostname const overviewUrl = overview[lang] - return NextResponse.redirect(new URL(overviewUrl, nextUrlClone)) + const redirectUrl = new URL(overviewUrl, nextUrlClone) + console.log(`[myPages] redirecting to: ${redirectUrl}`) + return NextResponse.redirect(redirectUrl) } const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}`, "") diff --git a/middlewares/utils.ts b/middlewares/utils.ts index a96045ed6..cc437133a 100644 --- a/middlewares/utils.ts +++ b/middlewares/utils.ts @@ -1,3 +1,6 @@ +import { env } from "@/env/server" +import { internalServerError } from "@/server/errors/next" + import { findLang } from "@/utils/languages" import { removeTrailingSlash } from "@/utils/url" @@ -6,6 +9,17 @@ import type { NextRequest } from "next/server" export function getDefaultRequestHeaders(request: NextRequest) { const lang = findLang(request.nextUrl.pathname)! + let nextUrl + if (env.PUBLIC_URL) { + const publicUrl = new URL(env.PUBLIC_URL) + const nextUrlPublic = request.nextUrl.clone() + nextUrlPublic.host = publicUrl.host + nextUrlPublic.hostname = publicUrl.hostname + nextUrl = nextUrlPublic + } else { + nextUrl = request.nextUrl + } + const headers = new Headers(request.headers) headers.set("x-lang", lang) headers.set( @@ -14,7 +28,7 @@ export function getDefaultRequestHeaders(request: NextRequest) { request.nextUrl.pathname.replace(`/${lang}`, "").replace(`/webview`, "") ) ) - headers.set("x-url", removeTrailingSlash(request.nextUrl.href)) + headers.set("x-url", removeTrailingSlash(nextUrl.href)) return headers } diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index c2cad2400..2f3f1c5c6 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -55,8 +55,14 @@ async function getVerifiedUser({ session }: { session: Session }) { return { error: true, cause: "unauthorized" } as const } else if (apiResponse.status === 403) { return { error: true, cause: "forbidden" } as const + } else if (apiResponse.status === 404) { + return { error: true, cause: "notfound" } as const } - return null + return { + error: true, + cause: "unknown", + status: apiResponse.status, + } as const } const apiJson = await apiResponse.json()