"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 { 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 { 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 }