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
188 lines
4.8 KiB
TypeScript
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
|
|
}
|