Files
web/apps/partner-sas/hooks/useSocialSession.ts
Hrishikesh Vaipurkar 1c7f72e95d Merged in fix/SW-3578-user-is-forced-to-login- (pull request #3044)
fix(SW-3578): Fixed session issue when sas session expires

* fix(SW-3578): Fixed session issue when sas session expires

* base socialLogin auto-features on validSession and being linked

* remove unused object

* remove 'import server-only' for isValidSession() since it's only using passed data

* remove isValidClientSession()


Approved-by: Joakim Jäderberg
Approved-by: Anton Gunnarsson
2025-11-03 12:50:25 +00:00

188 lines
4.8 KiB
TypeScript

"use client"
import { useMutation, useQuery } from "@tanstack/react-query"
import { useSession } from "next-auth/react"
import { useEffect } from "react"
import { dt } from "@scandic-hotels/common/dt"
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import type { User } from "next-auth"
import type { SocialSessionResponse } from "@/app/api/web/auth/scandic/session/route"
const logger = createLogger("useSocialSession")
export function useSocialSession() {
const socialSession = useSocialSessionQuery()
const refresh = useRefresh()
useAutoLogin()
return {
session: socialSession,
refresh,
}
}
function useSocialSessionQuery() {
const { data: session } = useSession()
const enabled = isValidSession(session) && isUserLinked(session?.user)
return useQuery({
queryKey: ["socialSession"],
queryFn: getSocialSession,
enabled: enabled,
refetchInterval: getTime(1, "m"),
})
}
const autoLoginLogger = createLogger("useAutoLogin")
function useAutoLogin() {
const { data: session } = useSession()
const { isSuccess, data: socialSession } = useSocialSessionQuery()
const isLinked = isUserLinked(session?.user)
useEffect(() => {
if (!isLinked) {
autoLoginLogger.info("User is not linked")
return
}
if (!isSuccess) {
autoLoginLogger.info("Social session is not loaded")
return
}
if (isSuccess && socialSession.status !== "no_session") {
autoLoginLogger.info("Social session is already active")
return
}
const expires = dt(session?.expires)
if (!expires || !expires.isValid()) {
autoLoginLogger.info("Session does not have a valid expiry")
return
}
const hasExpired = expires.isSameOrBefore(dt())
if (hasExpired) {
autoLoginLogger.info("Session has expired")
return
}
autoLoginLogger.info("Autologin to Curity")
// TODO: Check if we can do this silently without redirect
window.location.href =
"/api/web/auth/scandic/login?redirect_to=" + window.location.href
}, [isLinked, isSuccess, session?.expires, socialSession?.status])
}
function useRefresh() {
const socialSession = useSocialSessionQuery()
const refresh = useMutation({
mutationKey: ["refresh", "socialSession"],
mutationFn: refreshSession,
onSettled: () => {
socialSession.refetch()
},
})
useEffect(() => {
// Refresh the token if it has expired
if (socialSession.data?.status !== "expired") return
if (refresh.isPending) return
if (refresh.isError) return
logger.debug("Social session has expired, refreshing")
refresh.mutate()
}, [socialSession, refresh])
const expiresAt =
socialSession.data?.status === "valid" ? socialSession.data.expiresAt : null
useEffect(() => {
// Set up a timer to refresh the token 1 minute before it expires
if (!expiresAt) return
if (refresh.isPending) return
if (refresh.isError) return
const expiresAtDt = dt(expiresAt)
const timeToExpire = expiresAtDt.diff(dt(), "milliseconds")
const refreshAt = timeToExpire - getTime(1, "m")
if (refreshAt <= 0) {
// If it has already expired it's already being handled by the other useEffect
return
}
logger.debug(
`Refreshing social session at`,
dt().add(refreshAt, "milliseconds").toISOString()
)
const timeout = setTimeout(() => {
refresh.mutate()
}, refreshAt)
return () => clearTimeout(timeout)
}, [expiresAt, refresh])
return refresh
}
async function getSocialSession(): Promise<SocialSessionResponse> {
const response = await fetch("/api/web/auth/scandic/session", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
throw new Error("Failed to fetch social session", {
cause: { status: response.status, statusText: response.statusText },
})
}
const data = (await response.json()) as SocialSessionResponse
return data
}
async function refreshSession(): Promise<SocialSessionResponse> {
const response = await fetch("/api/web/auth/scandic/refresh", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
if (!response.ok) {
throw new Error("Failed to fetch social session", {
cause: { status: response.status, statusText: response.statusText },
})
}
const data = (await response.json()) as SocialSessionResponse
return data
}
function getTime(value: number, unit: "m" | "s") {
switch (unit) {
case "m":
return value * 60 * 1000
case "s":
return value * 1000
}
}
function isUserLinked(user: User | undefined): boolean {
if (user && "isLinked" in user) {
return !!user.isLinked
}
return false
}