Merged in feat/sw-2867-move-user-router-to-trpc-package (pull request #2428)

Move user router to trpc package

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Move booking router to trpc package

* Move partners router to trpc package

* Move autocomplete router to trpc package

* Move booking router to trpc package

* Remove translations from My Pages navigation trpc procedure

* Move navigation router to trpc package

* Move user router to trpc package

* Merge branch 'master' into feat/sw-2862-move-booking-router-to-trpc-package

* Merge branch 'feat/sw-2862-move-booking-router-to-trpc-package' into feat/sw-2865-move-navigation-router-to-trpc-package

* Merge branch 'master' into feat/sw-2865-move-navigation-router-to-trpc-package

* Merge branch 'master' into feat/sw-2865-move-navigation-router-to-trpc-package

* Merge branch 'master' into feat/sw-2865-move-navigation-router-to-trpc-package

* Merge branch 'feat/sw-2865-move-navigation-router-to-trpc-package' into feat/sw-2867-move-user-router-to-trpc-package

* Merge branch 'master' into feat/sw-2867-move-user-router-to-trpc-package


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-06-27 07:07:49 +00:00
parent 00bcdaaa28
commit 01ca2b4897
49 changed files with 609 additions and 562 deletions

View File

@@ -1,52 +0,0 @@
function maskAll(str: string) {
return "*".repeat(str.length)
}
function maskAllButFirstChar(str: string) {
const first = str[0]
const rest = str.substring(1)
const restMasked = maskAll(rest)
return `${first}${restMasked}`
}
function maskAllButLastTwoChar(str: string) {
const lastTwo = str.slice(-2)
const rest = str.substring(0, str.length - 2)
const restMasked = maskAll(rest)
return `${restMasked}${lastTwo}`
}
export function email(str: string) {
const parts = str.split("@")
const aliasMasked = maskAllButFirstChar(parts[0])
if (parts[1]) {
const domainParts = parts[1].split(".")
if (domainParts.length > 1) {
const domainTLD = domainParts.pop()
const domainPartsMasked = domainParts
.map((domainPart, i) => {
return maskAllButFirstChar(domainPart)
})
.join(".")
return `${aliasMasked}@${domainPartsMasked}.${domainTLD}`
}
}
return maskAllButFirstChar(str)
}
export function phone(str: string) {
return maskAllButLastTwoChar(str)
}
export function text(str: string) {
return maskAllButFirstChar(str)
}
export function all(str: string) {
return maskAll(str)
}

View File

@@ -1,27 +0,0 @@
import { describe, expect, test } from "@jest/globals"
import { all, email, phone, text } from "./maskValue"
describe("Mask value", () => {
test("masks e-mails properly", () => {
expect(email("test@example.com")).toBe("t***@e******.com")
expect(email("test@sub.example.com")).toBe("t***@s**.e******.com")
expect(email("test_no_atexample.com")).toBe("t********************")
expect(email("test_no_dot@examplecom")).toBe("t*********************")
expect(email("test_no_at_no_dot_com")).toBe("t********************")
})
test("masks phone number properly", () => {
expect(phone("0000000000")).toBe("********00")
})
test("masks text strings properly", () => {
expect(text("test")).toBe("t***")
expect(text("test.with.dot")).toBe("t************")
})
test("masks whole string properly", () => {
expect(all("test")).toBe("****")
expect(all("123jknasd@iajsd.c")).toBe("*****************")
})
})

View File

@@ -1,6 +1,5 @@
import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
@@ -240,52 +239,3 @@ export function serializeBookingSearchParams(
typeHints,
})
}
/**
* Returns the TLD (top-level domain) for a given language.
* @param lang - The language to get the TLD for
* @returns The TLD for the given language
*/
export function getTldForLanguage(lang: Lang): string {
switch (lang) {
case Lang.sv:
return "se"
case Lang.no:
return "no"
case Lang.da:
return "dk"
case Lang.fi:
return "fi"
case Lang.de:
return "de"
default:
return "com"
}
}
/**
* Constructs a URL with the correct TLD (top-level domain) based on lang, for current web.
* @param params - Object containing path, lang, and baseUrl
* @param params.path - The path to append to the URL
* @param params.lang - The language to use for TLD
* @param params.baseUrl - The base URL to use (e.g. https://www.scandichotels.com)
* @returns The complete URL with language-specific TLD
*/
export function getCurrentWebUrl({
path,
lang,
baseUrl = "https://www.scandichotels.com", // Fallback for ephemeral environments (e.g. deploy previews).
}: {
path: string
lang: Lang
baseUrl?: string
}): string {
const tld = getTldForLanguage(lang)
const url = new URL(path, baseUrl)
if (tld !== "com") {
url.host = url.host.replace(".com", `.${tld}`)
}
return url.toString()
}

View File

@@ -2,23 +2,8 @@ import {
type MembershipLevel,
MembershipLevelEnum,
} from "@scandic-hotels/common/constants/membershipLevels"
import { scandicMembershipTypes } from "@scandic-hotels/trpc/routers/user/helpers"
import type { User, UserLoyalty } from "@scandic-hotels/trpc/types/user"
export function getMembershipCards(userLoyalty: UserLoyalty) {
return userLoyalty.memberships
.filter(
(membership) => membership.type !== scandicMembershipTypes.SCANDIC_NATIVE
)
.map((membership) => ({
currentPoints: 0, // We only have points for Friends so we can't set this for now
expirationDate: membership.tierExpires,
membershipNumber: membership.membershipNumber,
membershipType: membership.type,
memberSince: membership.memberSince,
}))
}
import type { User } from "@scandic-hotels/trpc/types/user"
export function isHighestMembership(
membershipLevel: MembershipLevel | undefined

View File

@@ -1,46 +0,0 @@
import { z } from "zod"
export const passwordValidators = {
length: {
matcher: (password: string) =>
password.length >= 10 && password.length <= 40,
message: "10 to 40 characters",
},
hasUppercase: {
matcher: (password: string) => /[A-Z]/.test(password),
message: "1 uppercase letter",
},
hasLowercase: {
matcher: (password: string) => /[a-z]/.test(password),
message: "1 lowercase letter",
},
hasNumber: {
matcher: (password: string) => /[0-9]/.test(password),
message: "1 number",
},
hasSpecialChar: {
matcher: (password: string) =>
/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(password),
message: "1 special character",
},
}
export const passwordValidator = (msg = "Required field") =>
z
.string()
.min(1, msg)
.refine(passwordValidators.length.matcher, {
message: passwordValidators.length.message,
})
.refine(passwordValidators.hasUppercase.matcher, {
message: passwordValidators.hasUppercase.message,
})
.refine(passwordValidators.hasLowercase.matcher, {
message: passwordValidators.hasLowercase.message,
})
.refine(passwordValidators.hasNumber.matcher, {
message: passwordValidators.hasNumber.message,
})
.refine(passwordValidators.hasSpecialChar.matcher, {
message: passwordValidators.hasSpecialChar.message,
})

View File

@@ -1,26 +0,0 @@
import { z } from "zod"
export const phoneErrors = {
PHONE_NUMBER_TOO_SHORT: "PHONE_NUMBER_TOO_SHORT",
PHONE_REQUESTED: "PHONE_REQUESTED",
} as const
export function phoneValidator(
msg = "Required field",
invalidMsg = "Invalid type"
) {
return z
.string({ invalid_type_error: invalidMsg, required_error: msg })
.min(5, phoneErrors.PHONE_NUMBER_TOO_SHORT)
.superRefine((value, ctx) => {
if (value) {
const containsAlphabeticChars = /[a-z]/gi.test(value)
if (containsAlphabeticChars) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: phoneErrors.PHONE_REQUESTED,
})
}
}
})
}