Files
web/server/routers/user/query.ts
Niclas Edenvin 819ac454b0 Merged in feature/refresh-token (pull request #401)
feat: SW-101 Proactively refresh tokens

Approved-by: Michael Zetterberg
2024-08-16 13:56:09 +00:00

561 lines
17 KiB
TypeScript

import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import {
protectedProcedure,
router,
safeProtectedProcedure,
} from "@/server/trpc"
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
import * as maskValue from "@/utils/maskValue"
import { getMembership, getMembershipCards } from "@/utils/user"
import encryptValue from "../utils/encryptValue"
import { getUserInputSchema, staysInput } from "./input"
import {
getCreditCardsSchema,
getFriendTransactionsSchema,
getMembershipCardsSchema,
getStaysSchema,
getUserSchema,
Stay,
} from "./output"
import { benefits, extendedUser, nextLevelPerks } from "./temp"
import type { Session } from "next-auth"
import type {
LoginType,
TrackingSDKUserData,
} from "@/types/components/tracking"
async function getVerifiedUser({ session }: { session: Session }) {
const now = Date.now()
if (session.token.expires_at && session.token.expires_at < now) {
return { error: true, cause: "token_expired" } as const
}
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
})
if (!apiResponse.ok) {
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
}
return null
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
console.error(`User has no data - (user: ${JSON.stringify(session.user)})`)
return null
}
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
if (!verifiedData.success) {
console.error(
`Failed to validate User - (User: ${JSON.stringify(session.user)})`
)
console.error(verifiedData.error)
return null
}
return verifiedData
}
function fakingRequest<T>(payload: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(payload)
}, 1500)
})
}
const updateStaysBookingUrl = async (
data: Stay[],
token: string,
lang: Lang
) => {
// Tenporary API call needed till we have user name in ctx session data
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${token}`,
},
})
// Temporary Url, domain and lang support for current web
let localeDomain = env.PUBLIC_URL
let fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
switch (lang) {
case Lang.sv:
localeDomain = localeDomain?.replace(".com", ".se")
fullBookingUrl = localeDomain + "/hotelreservation/din-bokning"
break
case Lang.no:
localeDomain = localeDomain?.replace(".com", ".no")
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
break
case Lang.da:
localeDomain = localeDomain?.replace(".com", ".dk")
fullBookingUrl = localeDomain + "/hotelreservation/min-booking"
break
case Lang.fi:
localeDomain = localeDomain?.replace(".com", ".fi")
fullBookingUrl = localeDomain + "/varaa-hotelli/varauksesi"
break
case Lang.de:
localeDomain = localeDomain?.replace(".com", ".de")
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
break
default:
break
}
if (apiResponse.ok) {
const apiJson = await apiResponse.json()
if (apiJson.data?.attributes) {
return data.map((stay: Stay) => {
const originalString =
stay.attributes.confirmationNumber.toString() +
"," +
apiJson.data.attributes.lastName
const encryptedBookingValue = encryptValue(originalString)
const bookingUrl = !!encryptedBookingValue
? fullBookingUrl + "?RefId=" + encryptedBookingValue
: fullBookingUrl +
"?lastName=" +
apiJson.data.attributes.lastName +
"&bookingId=" +
stay.attributes.confirmationNumber
return {
...stay,
attributes: {
...stay.attributes,
bookingUrl: bookingUrl,
},
}
})
}
}
return data
}
export const userQueryRouter = router({
get: protectedProcedure
.input(getUserInputSchema)
.query(async function getUser({ ctx, input }) {
const data = await getVerifiedUser({ session: ctx.session })
if (!data) {
return null
}
if ("error" in data) {
return data
}
const verifiedData = data
const country = countries.find(
(c) => c.code === verifiedData.data.address.countryCode
)
const user = {
...extendedUser,
address: {
city: verifiedData.data.address.city,
country: country?.name ?? "",
countryCode: verifiedData.data.address.countryCode,
streetAddress: verifiedData.data.address.streetAddress,
zipCode: verifiedData.data.address.zipCode,
},
dateOfBirth: verifiedData.data.dateOfBirth,
email: verifiedData.data.email,
firstName: verifiedData.data.firstName,
language: verifiedData.data.language,
lastName: verifiedData.data.lastName,
memberships: verifiedData.data.memberships,
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
phoneNumber: verifiedData.data.phoneNumber,
profileId: verifiedData.data.profileId,
}
if (input.mask) {
if (user.address.city) {
user.address.city = maskValue.text(user.address.city)
}
if (user.address.streetAddress) {
user.address.streetAddress = maskValue.text(
user.address.streetAddress
)
}
user.address.zipCode = verifiedData.data.address?.zipCode
? maskValue.text(verifiedData.data.address.zipCode)
: ""
user.email = maskValue.email(user.email)
user.phoneNumber = user.phoneNumber
? maskValue.phone(user.phoneNumber)
: ""
}
return user
}),
name: safeProtectedProcedure.query(async function ({ ctx }) {
if (!ctx.session) {
return null
}
const verifiedData = await getVerifiedUser({ session: ctx.session })
if (!verifiedData || "error" in verifiedData) {
return null
}
return {
firstName: verifiedData.data.firstName,
lastName: verifiedData.data.lastName,
}
}),
membershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
if (!ctx.session) {
return null
}
const verifiedData = await getVerifiedUser({ session: ctx.session })
if (!verifiedData || "error" in verifiedData) {
return null
}
const membershipLevel = getMembership(verifiedData.data.memberships)
return membershipLevel
}),
tracking: safeProtectedProcedure.query(async function ({ ctx }) {
const notLoggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "Non-logged in",
}
if (!ctx.session) {
return notLoggedInUserTrackingData
}
const verifiedUserData = await getVerifiedUser({ session: ctx.session })
if (!verifiedUserData || "error" in verifiedUserData) {
return notLoggedInUserTrackingData
}
const params = new URLSearchParams()
params.set("limit", "1")
const previousStaysResponse = await api.get(
api.endpoints.v1.previousStays,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!previousStaysResponse.ok) {
console.error(
`API Response Failed - Getting Previous Stays for tracking user`
)
console.error(previousStaysResponse)
return notLoggedInUserTrackingData
}
const previousStaysApiJson = await previousStaysResponse.json()
const verifiedPreviousStaysData =
getStaysSchema.safeParse(previousStaysApiJson)
if (!verifiedPreviousStaysData.success) {
console.error(`Failed to validate Previous Stays Data for tracking user`)
console.error(verifiedPreviousStaysData.error)
return notLoggedInUserTrackingData
}
const membership = getMembership(verifiedUserData.data.memberships)
const loggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "logged in",
loginType: ctx.session.token.loginType as LoginType,
memberId: membership?.membershipNumber,
memberLevel: membership?.membershipLevel,
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
totalPointsAvailableToSpend: membership?.currentPoints,
loginAction: "login success",
}
return loggedInUserTrackingData
}),
benefits: router({
current: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof benefits>(benefits)
}),
nextLevel: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
}),
}),
stays: router({
previous: protectedProcedure
.input(staysInput)
.query(async ({ ctx, input }) => {
const { limit, cursor } = input
const params = new URLSearchParams()
params.set("limit", limit.toString())
if (cursor) {
params.set("offset", cursor.toString())
}
const apiResponse = await api.get(
api.endpoints.v1.previousStays,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError(apiResponse)
// case 401:
// throw unauthorizedError(apiResponse)
// case 403:
// throw forbiddenError(apiResponse)
// default:
// throw internalServerError(apiResponse)
// }
console.error(`API Response Failed - Getting Previous Stays`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.error(`Failed to validate Previous Stays Data`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
ctx.session.token.access_token,
ctx.lang
)
return {
data: updatedData,
nextCursor,
}
}),
upcoming: protectedProcedure
.input(staysInput)
.query(async ({ ctx, input }) => {
const { limit, cursor } = input
const params = new URLSearchParams()
params.set("limit", limit.toString())
if (cursor) {
params.set("offset", cursor.toString())
}
const apiResponse = await api.get(
api.endpoints.v1.upcomingStays,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
},
params
)
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError(apiResponse)
// case 401:
// throw unauthorizedError(apiResponse)
// case 403:
// throw forbiddenError(apiResponse)
// default:
// throw internalServerError(apiResponse)
// }
console.error(`API Response Failed - Getting Upcoming Stays`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getStaysSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.error(`Failed to validate Upcoming Stays Data`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const nextCursor =
verifiedData.data.links &&
verifiedData.data.links.offset < verifiedData.data.links.totalCount
? verifiedData.data.links.offset
: undefined
const updatedData = await updateStaysBookingUrl(
verifiedData.data.data,
ctx.session.token.access_token,
ctx.lang
)
return {
data: updatedData,
nextCursor,
}
}),
}),
transaction: router({
friendTransactions: protectedProcedure.query(async (opts) => {
const apiResponse = await api.get(api.endpoints.v1.friendTransactions, {
headers: {
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError()
// case 401:
// throw unauthorizedError()
// case 403:
// throw forbiddenError()
// default:
// throw internalServerError()
// }
console.error(`API Response Failed - Getting Friend Transactions`)
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.error(`Failed to validate Friend Transactions Data`)
console.error(`User: (${JSON.stringify(opts.ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
return {
data: verifiedData.data.data.map(({ attributes }) => {
return {
awardPoints: attributes.awardPoints,
checkinDate: attributes.checkinDate,
checkoutDate: attributes.checkoutDate,
city: attributes.hotelInformation?.city,
confirmationNumber: attributes.confirmationNumber,
hotelName: attributes.hotelInformation?.name,
nights: attributes.nights,
}
}),
}
}),
}),
creditCards: protectedProcedure.query(async function ({ ctx }) {
const apiResponse = await api.get(api.endpoints.v1.creditCards, {
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
console.error(`API Response Failed - Getting Creadit Cards`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = getCreditCardsSchema.safeParse(apiJson)
if (!verifiedData.success) {
console.error(`Failed to validate Credit Cards Data`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
return verifiedData.data.data
}),
membershipCards: protectedProcedure.query(async function ({ ctx }) {
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
// switch (apiResponse.status) {
// case 400:
// throw badRequestError()
// case 401:
// throw unauthorizedError()
// case 403:
// throw forbiddenError()
// default:
// throw internalServerError()
// }
console.error(`API Response Failed - Getting Membership Cards`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(apiResponse)
}
const apiJson = await apiResponse.json()
const verifiedData = getMembershipCardsSchema.safeParse(
apiJson.data.attributes.memberships
)
if (!verifiedData.success) {
console.error(`Failed to validate Memberships Cards Data`)
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
console.error(verifiedData.error)
return null
}
const cards = getMembershipCards(verifiedData.data)
return cards
}),
})