Merged in feature/curity-social-login (pull request #2963)
feat(SW-3541): Do social login after login to SAS * feat(auth): wip social login via curity * Setup social login auth flow * Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/curity-social-login * Added support for getting scandic tokens and refresh them * feat: Enhance social login and session management with auto-refresh and improved error handling * Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/curity-social-login * wrap layout in suspense * revert app/layout.tsx * fix import * cleanup * merge * merge * dont pass client_secret in the url to curity * add state validation when doing social login through /authorize * remove debug logging Approved-by: Anton Gunnarsson
This commit is contained in:
184
apps/partner-sas/hooks/useSocialSession.ts
Normal file
184
apps/partner-sas/hooks/useSocialSession.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
"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 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()
|
||||
return useQuery({
|
||||
queryKey: ["socialSession"],
|
||||
queryFn: getSocialSession,
|
||||
enabled: !!session,
|
||||
refetchInterval: getTime(1, "m"),
|
||||
})
|
||||
}
|
||||
const autoLoginLogger = createLogger("useAutoLogin")
|
||||
function useAutoLogin() {
|
||||
const { data: session } = useSession()
|
||||
const { isSuccess, data: socialSession } = useSocialSessionQuery()
|
||||
|
||||
const isLinked = isLinkedUser(session?.user) ? session.user.isLinked : false
|
||||
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 isLinkedUser(
|
||||
user: User | undefined
|
||||
): user is User & { isLinked: boolean } {
|
||||
if (user && "isLinked" in user) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user