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:
@@ -1,12 +1,57 @@
|
|||||||
"use client"
|
"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() {
|
export function SessionRefresher() {
|
||||||
const session = useSession()
|
useSilentAuth()
|
||||||
if (session.data?.error === "RefreshAccessTokenError") {
|
useHandleRefreshError()
|
||||||
signOut({ redirect: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
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 }
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ const config: NextAuthConfig = {
|
|||||||
async signIn() {
|
async signIn() {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|
||||||
async jwt(params) {
|
async jwt(params) {
|
||||||
if (params.trigger === "signIn") {
|
if (params.trigger === "signIn") {
|
||||||
const accessToken = params.account?.access_token
|
const accessToken = params.account?.access_token
|
||||||
@@ -187,34 +188,45 @@ const config: NextAuthConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async redirect({ baseUrl, url }) {
|
async redirect({ baseUrl, url }) {
|
||||||
authLogger.debug(`[auth] deciding redirect URL`, { baseUrl, url })
|
authLogger.debug(`[redirect callback] deciding redirect URL`, {
|
||||||
|
baseUrl,
|
||||||
|
url,
|
||||||
|
})
|
||||||
if (url.startsWith("/")) {
|
if (url.startsWith("/")) {
|
||||||
authLogger.debug(
|
authLogger.debug(
|
||||||
`[auth] relative URL accepted, returning: ${baseUrl}${url}`
|
`[redirect callback] relative URL accepted, returning: ${baseUrl}${url}`
|
||||||
)
|
)
|
||||||
// Allows relative callback URLs
|
// Allows relative callback URLs
|
||||||
return `${baseUrl}${url}`
|
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
|
return baseUrl
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import * as Sentry from "@sentry/nextjs"
|
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 { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
import { logger } from "@scandic-hotels/common/logger"
|
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 bookingFlow from "@/middlewares/bookingFlow"
|
||||||
import * as invalidUrl from "@/middlewares/invalidUrl"
|
import * as invalidUrl from "@/middlewares/invalidUrl"
|
||||||
|
import * as silentAuthMiddleware from "@/middlewares/silentAuthMiddleware"
|
||||||
import * as trailingSlash from "@/middlewares/trailingSlash"
|
import * as trailingSlash from "@/middlewares/trailingSlash"
|
||||||
import { getDefaultRequestHeaders } from "@/middlewares/utils"
|
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)
|
request.headers.set("x-sh-origin", request.nextUrl.origin)
|
||||||
|
|
||||||
const headers = getDefaultRequestHeaders(request)
|
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) {
|
if (!lang) {
|
||||||
// Lang is required for all our middleware.
|
// Lang is required for all page middleware.
|
||||||
// Without it we shortcircuit early.
|
// Without it we shortcircuit early.
|
||||||
|
|
||||||
// Default to English if no lang is found.
|
// 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.
|
// 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
|
middleware: NextMiddleware
|
||||||
matcher: MiddlewareMatcher
|
matcher: MiddlewareMatcher
|
||||||
}[] = [
|
}[]
|
||||||
invalidUrl,
|
request: NextRequest
|
||||||
trailingSlash,
|
event: NextFetchEvent
|
||||||
// authRequired,
|
lang?: Lang
|
||||||
// handleAuth,
|
defaultHeaders: Headers
|
||||||
bookingFlow,
|
}) {
|
||||||
// cmsContent,
|
|
||||||
]
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < middlewares.length; ++i) {
|
for (let i = 0; i < middlewares.length; ++i) {
|
||||||
const middleware = middlewares[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),
|
new URL(`/${lang}/middleware-error/${e.status}`, request.nextUrl),
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
headers,
|
headers: defaultHeaders,
|
||||||
},
|
},
|
||||||
status: e.status,
|
status: e.status,
|
||||||
statusText: e.statusText,
|
statusText: e.statusText,
|
||||||
@@ -88,20 +128,13 @@ export const middleware: NextMiddleware = async (request, event) => {
|
|||||||
new URL(`/${lang}/middleware-error/500`, request.nextUrl),
|
new URL(`/${lang}/middleware-error/500`, request.nextUrl),
|
||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
headers,
|
headers: defaultHeaders,
|
||||||
},
|
},
|
||||||
status: 500,
|
status: 500,
|
||||||
statusText: "Internal Server Error",
|
statusText: "Internal Server Error",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow through with normal App router rules.
|
|
||||||
return NextResponse.next({
|
|
||||||
request: {
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
@@ -110,5 +143,8 @@ export const config = {
|
|||||||
* public routes inside middleware.
|
* 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)
|
* (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",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
27
apps/partner-sas/middlewares/silentAuthMiddleware.ts
Normal file
27
apps/partner-sas/middlewares/silentAuthMiddleware.ts
Normal 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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user