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:
Joakim Jäderberg
2025-10-16 12:47:12 +00:00
parent 1850cfd20d
commit 291310e841
24 changed files with 827 additions and 84 deletions

View File

@@ -2,13 +2,7 @@
import { usePathname, useSearchParams } from "next/navigation"
import { parseAsInteger, useQueryState } from "nuqs"
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react"
import { createContext, useCallback, useContext, useMemo } from "react"
import { type IntlShape, useIntl } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
@@ -76,8 +70,7 @@ export function SelectRateProvider({
parseAsInteger.withDefault(0)
)
const [_bookingCodeFilter, setBookingCodeFilter] =
useState<BookingCodeFilterEnum>(BookingCodeFilterEnum.Discounted)
const [_bookingCodeFilter, setBookingCodeFilter] = useBookingCodeFilter()
const selectRateBooking = parseSelectRateSearchParams(
searchParamsToRecord(searchParams)
@@ -313,7 +306,7 @@ export function SelectRateProvider({
_bookingCodeFilter === BookingCodeFilterEnum.Discounted &&
!selectRateInput.data?.booking.bookingCode
? BookingCodeFilterEnum.All
: _bookingCodeFilter
: (_bookingCodeFilter as BookingCodeFilterEnum)
const roomAvailabilityWithAdjustedRoomCount: (AvailabilityWithRoomInfo | null)[][] =
roomAvailability.map((availability, roomIndex) => {
@@ -557,12 +550,15 @@ function getAvailabilityForRoom(
function useUpdateBooking() {
const pathname = usePathname()
const [bookingCodeFilter] = useBookingCodeFilter()
return function updateBooking(booking: SelectRateBooking) {
const newUrl = new URL(pathname, window.location.origin)
// TODO: Handle existing search params
newUrl.search = serializeBookingSearchParams(booking).toString()
newUrl.searchParams.set(BookingCodeFilterQueryName, bookingCodeFilter)
// router.replace(newUrl.toString(), { scroll: false })
window.history.replaceState({}, "", newUrl.toString())
}
@@ -575,3 +571,10 @@ function isRoomPackage(x: {
x.code as RoomPackageCodeEnum
)
}
const BookingCodeFilterQueryName = "bookingCodeFilter"
function useBookingCodeFilter() {
return useQueryState(BookingCodeFilterQueryName, {
defaultValue: BookingCodeFilterEnum.Discounted,
})
}

View File

@@ -5,7 +5,7 @@ import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { env } from "../../../../env/server"
import { protectedProcedure } from "../../../procedures"
import type { Session } from "next-auth"
import type { LoginType } from "@scandic-hotels/common/constants/loginType"
const outputSchema = z.object({
eurobonusNumber: z.string(),
@@ -48,28 +48,39 @@ const outputSchema = z.object({
const sasLogger = createLogger("SAS")
const url = new URL("/api/scandic-partnership/v1/profile", env.SAS_API_ENDPOINT)
export async function getEuroBonusProfileData(session: Session) {
if (session.token.loginType !== "sas") {
return {
error: {
message: `Failed to fetch EuroBonus profile, expected loginType to be "sas" but was ${session.token.loginType}`,
},
} as const
const requiredLoginType: LoginType[] = ["sas"]
export const getEuroBonusProfile = protectedProcedure
.output(outputSchema)
.query(async function ({ ctx }) {
return await getEuroBonusProfileData({
accessToken: ctx.session.token.access_token,
loginType: ctx.session.token.loginType,
})
})
export async function getEuroBonusProfileData({
accessToken,
loginType,
}: {
loginType: LoginType
accessToken: string
}) {
if (!accessToken) {
throw new Error("Access token is required to fetch EuroBonus profile")
}
if (!session.token.expires_at || session.token.expires_at < Date.now()) {
return {
error: {
message: "Token expired sas",
},
} as const
if (!requiredLoginType.includes(loginType)) {
throw new Error(
`Failed to fetch EuroBonus profile, expected loginType to be "${requiredLoginType}" but was "${loginType}"`
)
}
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
"Ocp-Apim-Subscription-Key": env.SAS_OCP_APIM,
Authorization: `Bearer ${session?.token?.access_token}`,
Authorization: `Bearer ${accessToken}`,
},
})
@@ -77,12 +88,9 @@ export async function getEuroBonusProfileData(session: Session) {
sasLogger.error(
`Failed to get EuroBonus profile, status: ${response.status}, statusText: ${response.statusText}`
)
return {
error: {
message: "Failed to fetch EuroBonus profile",
cause: { status: response.status, statusText: response.statusText },
},
} as const
throw new Error("Failed to fetch EuroBonus profile", {
cause: { status: response.status, statusText: response.statusText },
})
}
const responseJson = await response.json()
@@ -91,24 +99,8 @@ export async function getEuroBonusProfileData(session: Session) {
sasLogger.error(
`Failed to parse EuroBonus profile, cause: ${data.error.cause}, message: ${data.error.message}`
)
return {
error: {
message: `Failed to parse EuroBonus profile: ${data.error.message}`,
cause: { status: response.status, statusText: response.statusText },
},
} as const
throw new Error(`Failed to parse EuroBonus profile: ${data.error.message}`)
}
return data
}
export const getEuroBonusProfile = protectedProcedure.query(async function ({
ctx,
}) {
const verifiedSasUser = await getEuroBonusProfileData(ctx.session)
if ("error" in verifiedSasUser) {
throw new Error(verifiedSasUser.error?.message, {
cause: verifiedSasUser.error?.cause,
})
}
return verifiedSasUser.data
})
return data.data
}

View File

@@ -11,7 +11,10 @@ export async function getUserPointsBalance(
const verifiedUser =
session.token.loginType === "sas"
? await getEuroBonusProfileData(session)
? await getEuroBonusProfileData({
accessToken: session.token.access_token,
loginType: session.token.loginType,
})
: await getVerifiedUser({ session })
if (!verifiedUser || "error" in verifiedUser) {
@@ -19,8 +22,8 @@ export async function getUserPointsBalance(
}
const points =
"points" in verifiedUser.data
? verifiedUser.data.points.total
"points" in verifiedUser
? verifiedUser.points.total
: verifiedUser.data.membership?.currentPoints
return points ?? 0

View File

@@ -67,6 +67,8 @@
},
"peerDependencies": {
"@sentry/nextjs": "^10",
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"next": "^15",
"react": "^19"
},