diff --git a/app/modules/asset/service.server.ts b/app/modules/asset/service.server.ts index 8a9b93f90..f39bd4d4a 100644 --- a/app/modules/asset/service.server.ts +++ b/app/modules/asset/service.server.ts @@ -2573,8 +2573,9 @@ export async function bulkCheckOutAssets({ throw new ShelfError({ cause: null, message: - "There are sone unavailable assets. Please make sure you are selecting only available assets.", + "There are some unavailable assets. Please make sure you are selecting only available assets.", label: "Assets", + shouldBeCaptured: false, }); } @@ -2670,6 +2671,7 @@ export async function bulkCheckInAssets({ message: "There are some assets without custody. Please make sure you are selecting assets with custody.", label: "Assets", + shouldBeCaptured: false, }); } @@ -2983,6 +2985,7 @@ export async function relinkQrCode({ message: "You cannot link to this code because its already linked to another asset. Delete the other asset to free up the code and try again.", label: "QR", + shouldBeCaptured: false, }); } diff --git a/app/modules/team-member/service.server.ts b/app/modules/team-member/service.server.ts index 235b7f89d..8011518e1 100644 --- a/app/modules/team-member/service.server.ts +++ b/app/modules/team-member/service.server.ts @@ -392,18 +392,39 @@ async function fixTeamMembersNames(teamMembers: TeamMemberWithUserData[]) { if (teamMembersWithEmptyNames.length === 0) return; /** - * Itterate over the members and update them without awaiting - * Just in case we check again + * Updates team member names by: + * 1. Using first + last name if both exist + * 2. Using just first or last name if one exists + * 3. Falling back to email username if no name exists + * 4. Using "Unknown" as last resort if no email exists */ await Promise.all( teamMembersWithEmptyNames.map((teamMember) => { - const name = teamMember.user - ? `${teamMember.user.firstName} ${teamMember.user.lastName}` - : "Unknown name"; - return db.teamMember.update({ - where: { id: teamMember.id }, - data: { name }, - }); + let name: string; + + if (teamMember.user) { + const { firstName, lastName, email } = teamMember.user; + + if (firstName?.trim() || lastName?.trim()) { + // At least one name exists - concatenate available names + name = [firstName?.trim(), lastName?.trim()] + .filter(Boolean) + .join(" "); + } else { + // No names but email exists - use email username + name = email.split("@")[0]; + // Optionally improve email username readability + name = name + .replace(/[._]/g, " ") // Replace dots/underscores with spaces + .replace(/\b\w/g, (c) => c.toUpperCase()); // Capitalize words + } + + return db.teamMember.update({ + where: { id: teamMember.id }, + data: { name }, + }); + } + return null; }) ); diff --git a/app/modules/user/utils.server.ts b/app/modules/user/utils.server.ts index fb2a4807a..e380bd47f 100644 --- a/app/modules/user/utils.server.ts +++ b/app/modules/user/utils.server.ts @@ -6,6 +6,7 @@ import { sendEmail } from "~/emails/mail.server"; import { sendNotification } from "~/utils/emitter/send-notification.server"; import { ShelfError } from "~/utils/error"; import { data, parseData } from "~/utils/http.server"; +import { randomUsernameFromEmail } from "~/utils/user"; import { revokeAccessToOrganization } from "./service.server"; import { revokeAccessEmailText } from "../invite/helpers"; import { createInvite } from "../invite/service.server"; @@ -215,3 +216,42 @@ export async function resolveUserAction( } } } + +/** + * Maximum number of attempts to generate a unique username + * This prevents infinite loops while still providing multiple retry attempts + */ +const MAX_USERNAME_ATTEMPTS = 5; + +/** + * Generates a unique username for a new user with retry mechanism + * @param email - User's email to base username on + * @returns Unique username or throws if cannot generate after max attempts + * @throws {ShelfError} If unable to generate unique username after max attempts + */ +export async function generateUniqueUsername(email: string): Promise { + let attempts = 0; + + while (attempts < MAX_USERNAME_ATTEMPTS) { + const username = randomUsernameFromEmail(email); + + // Check if username exists + const existingUser = await db.user.findUnique({ + where: { username }, + select: { id: true }, + }); + + if (!existingUser) { + return username; + } + + attempts++; + } + + throw new ShelfError({ + cause: null, + message: "Unable to generate unique username after maximum attempts", + label: "User", + additionalData: { email, attempts: MAX_USERNAME_ATTEMPTS }, + }); +} diff --git a/app/routes/_auth+/otp.tsx b/app/routes/_auth+/otp.tsx index 5dae72d5e..dfeb626bc 100644 --- a/app/routes/_auth+/otp.tsx +++ b/app/routes/_auth+/otp.tsx @@ -16,6 +16,7 @@ import { verifyOtpAndSignin } from "~/modules/auth/service.server"; import { setSelectedOrganizationIdCookie } from "~/modules/organization/context.server"; import { getOrganizationByUserId } from "~/modules/organization/service.server"; import { createUser, findUserByEmail } from "~/modules/user/service.server"; +import { generateUniqueUsername } from "~/modules/user/utils.server"; import { appendToMetaTitle } from "~/utils/append-to-meta-title"; import { setCookie } from "~/utils/cookies.server"; import { makeShelfError, notAllowedMethod } from "~/utils/error"; @@ -30,7 +31,6 @@ import { import { validEmail } from "~/utils/misc"; import { getOtpPageData, type OtpVerifyMode } from "~/utils/otp"; import { tw } from "~/utils/tw"; -import { randomUsernameFromEmail } from "~/utils/user"; import type { action as resendOtpAction } from "./resend-otp"; export function loader({ context, request }: LoaderFunctionArgs) { @@ -70,9 +70,10 @@ export async function action({ context, request }: ActionFunctionArgs) { const userExists = Boolean(await findUserByEmail(email)); if (!userExists) { + const username = await generateUniqueUsername(authSession.email); await createUser({ ...authSession, - username: randomUsernameFromEmail(authSession.email), + username, }); } diff --git a/app/routes/_layout+/account-details.general.tsx b/app/routes/_layout+/account-details.general.tsx index c358ac9c2..398f2b299 100644 --- a/app/routes/_layout+/account-details.general.tsx +++ b/app/routes/_layout+/account-details.general.tsx @@ -239,6 +239,7 @@ export async function action({ context, request }: ActionFunctionArgs) { : "Failed to initiate email change", additionalData: { userId, newEmail }, label: "Auth", + shouldBeCaptured: !emailExists, }); }