Merged in feat/SW-3639-autologin-sas (pull request #3245)

Feat/SW-3639 autologin sas

* wip

* cleanup

* remove commented code and default lang to EN


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-11-28 13:00:42 +00:00
parent e621570f99
commit 9294f0958b
4 changed files with 170 additions and 50 deletions

View File

@@ -1,12 +1,57 @@
"use client"
import { signOut, useSession } from "next-auth/react"
import { signIn, signOut, useSession } from "next-auth/react"
import { useEffect, useState } from "react"
import { useLocalStorage } from "usehooks-ts"
export function SessionRefresher() {
const session = useSession()
if (session.data?.error === "RefreshAccessTokenError") {
signOut({ redirect: false })
}
useSilentAuth()
useHandleRefreshError()
return null
}
function useHandleRefreshError() {
const session = useSession()
if (session.data?.error === "RefreshAccessTokenError") {
signOut({ redirect: false })
}
}
const SILENT_AUTH_KEY = "silent-auth"
const SILENT_AUTH_EXPIRY = 6 * 60 * 1000 // 6 hours
function useSilentAuth() {
const { status } = useSession()
const [silentAuthTimestamp, setSilentAuthTimestamp] = useLocalStorage<
string | undefined
>(SILENT_AUTH_KEY, undefined)
const [isLoading, setLoading] = useState(() => status === "unauthenticated")
const hasCompletedSilentSignin =
!!silentAuthTimestamp &&
Date.now() - Number(silentAuthTimestamp) < SILENT_AUTH_EXPIRY
useEffect(() => {
if (status !== "unauthenticated") return
if (hasCompletedSilentSignin) return
setLoading(true)
try {
signIn(
"sas",
{},
{
prompt: "none",
}
)
} finally {
setSilentAuthTimestamp(Date.now().toString())
setLoading(false)
}
}, [hasCompletedSilentSignin, setSilentAuthTimestamp, status])
return { isLoading: status !== "authenticated" && isLoading }
}

View File

@@ -115,6 +115,7 @@ const config: NextAuthConfig = {
async signIn() {
return true
},
async jwt(params) {
if (params.trigger === "signIn") {
const accessToken = params.account?.access_token
@@ -187,34 +188,45 @@ const config: NextAuthConfig = {
}
},
async redirect({ baseUrl, url }) {
authLogger.debug(`[auth] deciding redirect URL`, { baseUrl, url })
authLogger.debug(`[redirect callback] deciding redirect URL`, {
baseUrl,
url,
})
if (url.startsWith("/")) {
authLogger.debug(
`[auth] relative URL accepted, returning: ${baseUrl}${url}`
`[redirect callback] relative URL accepted, returning: ${baseUrl}${url}`
)
// Allows relative callback URLs
return `${baseUrl}${url}`
} else {
// Assume absolute URL
try {
const parsedUrl = new URL(url)
if (parsedUrl.hostname.endsWith(".scandichotels.com")) {
authLogger.debug(`[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
authLogger.debug(`[auth] origin URL accepted, returning: ${url}`)
return url
}
} catch (e) {
authLogger.error(
`[auth] error parsing incoming URL for redirection`,
e
)
}
}
authLogger.debug(`[auth] URL denied, returning base URL: ${baseUrl}`)
// Assume absolute URL
try {
const parsedUrl = new URL(url)
if (parsedUrl.hostname.endsWith(".scandichotels.com")) {
authLogger.debug(
`[redirect callback] subdomain URL accepted, returning: ${url}`
)
// Allows any subdomains on all top level domains above
return url
}
if (parsedUrl.origin === baseUrl) {
// Allows callback URLs on the same origin
authLogger.debug(
`[redirect callback] origin URL accepted, returning: ${url}`
)
return url
}
} catch (e) {
authLogger.error(
`[redirect callback] error parsing incoming URL for redirection`,
e
)
}
authLogger.debug(
`[redirect callback] URL denied, returning base URL: ${baseUrl}`
)
return baseUrl
},

View File

@@ -1,5 +1,10 @@
import * as Sentry from "@sentry/nextjs"
import { type NextMiddleware, NextResponse } from "next/server"
import {
type NextFetchEvent,
type NextMiddleware,
type NextRequest,
NextResponse,
} from "next/server"
import { Lang } from "@scandic-hotels/common/constants/language"
import { logger } from "@scandic-hotels/common/logger"
@@ -7,6 +12,7 @@ import { findLang } from "@scandic-hotels/common/utils/languages"
import * as bookingFlow from "@/middlewares/bookingFlow"
import * as invalidUrl from "@/middlewares/invalidUrl"
import * as silentAuthMiddleware from "@/middlewares/silentAuthMiddleware"
import * as trailingSlash from "@/middlewares/trailingSlash"
import { getDefaultRequestHeaders } from "@/middlewares/utils"
@@ -18,10 +24,20 @@ export const middleware: NextMiddleware = async (request, event) => {
request.headers.set("x-sh-origin", request.nextUrl.origin)
const headers = getDefaultRequestHeaders(request)
const lang = findLang(request.nextUrl.pathname)
const apiMiddlewareResults = await executeMiddlewares({
request,
event,
defaultHeaders: headers,
middlewares: [silentAuthMiddleware],
})
if (apiMiddlewareResults) {
return apiMiddlewareResults
}
const lang = findLang(request.nextUrl.pathname)
if (!lang) {
// Lang is required for all our middleware.
// Lang is required for all page middleware.
// Without it we shortcircuit early.
// Default to English if no lang is found.
@@ -34,18 +50,42 @@ export const middleware: NextMiddleware = async (request, event) => {
}
// Note that the order of middlewares is important since that is the order they are matched by.
const middlewares: {
const pageMiddlewareResults = await executeMiddlewares({
request,
event,
lang,
defaultHeaders: headers,
middlewares: [invalidUrl, trailingSlash, bookingFlow],
})
if (pageMiddlewareResults) {
return pageMiddlewareResults
}
// Follow through with normal App router rules.
return NextResponse.next({
request: {
headers,
},
})
}
async function executeMiddlewares({
middlewares,
request,
event,
lang = Lang.en,
defaultHeaders,
}: {
middlewares: {
middleware: NextMiddleware
matcher: MiddlewareMatcher
}[] = [
invalidUrl,
trailingSlash,
// authRequired,
// handleAuth,
bookingFlow,
// cmsContent,
]
}[]
request: NextRequest
event: NextFetchEvent
lang?: Lang
defaultHeaders: Headers
}) {
try {
for (let i = 0; i < middlewares.length; ++i) {
const middleware = middlewares[i]
@@ -73,7 +113,7 @@ export const middleware: NextMiddleware = async (request, event) => {
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
{
request: {
headers,
headers: defaultHeaders,
},
status: e.status,
statusText: e.statusText,
@@ -88,20 +128,13 @@ export const middleware: NextMiddleware = async (request, event) => {
new URL(`/${lang}/middleware-error/500`, request.nextUrl),
{
request: {
headers,
headers: defaultHeaders,
},
status: 500,
statusText: "Internal Server Error",
}
)
}
// Follow through with normal App router rules.
return NextResponse.next({
request: {
headers,
},
})
}
export const config = {
@@ -110,5 +143,8 @@ 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|_static|.netlify|api|trpc|sitemap).*)"],
matcher: [
"/((?!.+\\.[\\w]+$|_next|_static|.netlify|api|trpc|sitemap).*)",
"/api/web/auth/callback/sas",
],
}

View File

@@ -0,0 +1,27 @@
import {
type NextMiddleware,
type NextRequest,
NextResponse,
} from "next/server"
import type { MiddlewareMatcher } from "./types"
export const middleware: NextMiddleware = async (req) => {
const redirectUrl = loginRequiredRedirect(req)
if (redirectUrl) {
return NextResponse.redirect(redirectUrl)
}
}
function loginRequiredRedirect(req: NextRequest) {
if (req.nextUrl.searchParams.get("error") === "login_required") {
return (
req.cookies.get("next-auth.callback-url")?.value || req.nextUrl.origin
)
}
return undefined
}
export const matcher: MiddlewareMatcher = (request) => {
return request.nextUrl.pathname === "/api/web/auth/callback/sas"
}