diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts index 6006e48b..76a895a6 100644 --- a/server/private/routers/approvals/listApprovals.ts +++ b/server/private/routers/approvals/listApprovals.ts @@ -80,7 +80,8 @@ async function queryApprovals( user: { name: users.name, userId: users.userId, - username: users.username + username: users.username, + email: users.email } }) .from(approvals) diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 662ada60..c1036373 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -1,5 +1,6 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { ListUsersResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import UsersTable, { UserRow } from "../../../../../components/UsersTable"; @@ -73,7 +74,11 @@ export default async function UsersPage(props: UsersPageProps) { return { id: user.id, username: user.username, - displayUsername: user.email || user.name || user.username, + displayUsername: getUserDisplayName({ + email: user.email, + name: user.name, + username: user.username + }), name: user.name, email: user.email, type: user.type, diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index b26d79a9..3dedea05 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -40,6 +40,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries, resourceQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; @@ -154,7 +155,10 @@ export default function ResourceAuthenticationPage() { const allUsers = useMemo(() => { return orgUsers.map((user) => ({ id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + text: `${getUserDisplayName({ + email: user.email, + username: user.username + })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` })); }, [orgUsers]); @@ -229,7 +233,10 @@ export default function ResourceAuthenticationPage() { "users", resourceUsers.map((i) => ({ id: i.userId.toString(), - text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` + text: `${getUserDisplayName({ + email: i.email, + username: i.username + })}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` })) ); diff --git a/src/app/admin/users/[userId]/layout.tsx b/src/app/admin/users/[userId]/layout.tsx index 0c8c50e6..e773a2ec 100644 --- a/src/app/admin/users/[userId]/layout.tsx +++ b/src/app/admin/users/[userId]/layout.tsx @@ -2,6 +2,7 @@ import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { AdminGetUserResponse } from "@server/routers/user/adminGetUser"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { cache } from "react"; @@ -44,7 +45,15 @@ export default async function UserLayoutProps(props: UserLayoutProps) { return ( <> {children} diff --git a/src/app/auth/login/device/page.tsx b/src/app/auth/login/device/page.tsx index 2fa5fac5..9b6b2bd2 100644 --- a/src/app/auth/login/device/page.tsx +++ b/src/app/auth/login/device/page.tsx @@ -1,6 +1,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import DeviceLoginForm from "@/components/DeviceLoginForm"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { cache } from "react"; export const dynamic = "force-dynamic"; @@ -24,7 +25,12 @@ export default async function DeviceLoginPage({ searchParams }: Props) { ); } - const userName = user?.name || user?.username || ""; + const userName = user + ? getUserDisplayName({ + name: user.name, + username: user.username + }) + : ""; return (

{t("userQuestionRemove", { - selectedUser: - selected?.email || - selected?.name || - selected?.username + selectedUser: selected + ? getUserDisplayName({ + email: selected.email, + name: selected.name, + username: selected.username + }) + : "" })}

@@ -337,9 +341,11 @@ export default function UsersTable({ users }: Props) { } buttonText={t("userDeleteConfirm")} onConfirm={async () => deleteUser(selected!.id)} - string={ - selected.email || selected.name || selected.username - } + string={getUserDisplayName({ + email: selected.email, + name: selected.name, + username: selected.username + })} title={t("userDeleteServer")} /> )} diff --git a/src/components/ApprovalFeed.tsx b/src/components/ApprovalFeed.tsx index b3d446b1..974a02b2 100644 --- a/src/components/ApprovalFeed.tsx +++ b/src/components/ApprovalFeed.tsx @@ -2,6 +2,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { cn } from "@app/lib/cn"; import { approvalFiltersSchema, @@ -185,7 +186,11 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) { - {approval.user.username} + {getUserDisplayName({ + email: approval.user.email, + name: approval.user.name, + username: approval.user.username + })}   {approval.type === "user_device" && ( diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 2c9efdf5..4c8d6325 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -46,6 +46,7 @@ import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { cn } from "@app/lib/cn"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -270,7 +271,10 @@ export default function CreateInternalResourceDialog({ const allUsers = usersResponse.map((user) => ({ id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + text: `${getUserDisplayName({ + email: user.email, + username: user.username + })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` })); const allClients = clientsResponse diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 64d29909..4b5030be 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -36,6 +36,7 @@ import { import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { UserType } from "@server/types/UserTypes"; @@ -304,7 +305,10 @@ export default function EditInternalResourceDialog({ const allUsers = (usersQuery.data ?? []).map((user) => ({ id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + text: `${getUserDisplayName({ + email: user.email, + username: user.username + })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` })); const machineClients = (clientsQuery.data ?? []) @@ -330,7 +334,10 @@ export default function EditInternalResourceDialog({ const formUsers = (resourceUsersQuery.data ?? []).map((i) => ({ id: i.userId.toString(), - text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` + text: `${getUserDisplayName({ + email: i.email, + username: i.username + })}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` })); return { diff --git a/src/components/ProfileIcon.tsx b/src/components/ProfileIcon.tsx index b35fae35..d466b707 100644 --- a/src/components/ProfileIcon.tsx +++ b/src/components/ProfileIcon.tsx @@ -14,6 +14,7 @@ import { import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { Laptop, LogOut, Moon, Sun, Smartphone } from "lucide-react"; import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; @@ -49,9 +50,8 @@ export default function ProfileIcon() { const t = useTranslations(); function getInitials() { - return (user.email || user.name || user.username) - .substring(0, 1) - .toUpperCase(); + const displayName = getUserDisplayName({ user }); + return displayName.substring(0, 1).toUpperCase(); } function handleThemeChange(theme: "light" | "dark" | "system") { @@ -109,7 +109,7 @@ export default function ProfileIcon() { {t("signingAs")}

- {user.email || user.name || user.username} + {getUserDisplayName({ user })}

{user.serverAdmin ? ( diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 102014e3..11e5bead 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -12,6 +12,7 @@ import { import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { ArrowRight, ArrowUpDown, @@ -344,7 +345,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { href={`/${r.orgId}/settings/access/users/${r.userId}`} > diff --git a/src/components/UsersTable.tsx b/src/components/UsersTable.tsx index e8729a9d..d6b6e610 100644 --- a/src/components/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -19,6 +19,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; @@ -271,10 +272,13 @@ export default function UsersTable({ users: u }: UsersTableProps) { buttonText={t("userRemoveOrgConfirm")} onConfirm={removeUser} string={ - selectedUser?.email || - selectedUser?.name || - selectedUser?.username || - "" + selectedUser + ? getUserDisplayName({ + email: selectedUser.email, + name: selectedUser.name, + username: selectedUser.username + }) + : "" } title={t("userRemoveOrg")} /> diff --git a/src/lib/getUserDisplayName.ts b/src/lib/getUserDisplayName.ts new file mode 100644 index 00000000..e95096c1 --- /dev/null +++ b/src/lib/getUserDisplayName.ts @@ -0,0 +1,36 @@ +import { GetUserResponse } from "@server/routers/user"; + +type UserDisplayNameInput = + | { + user: GetUserResponse; + } + | { + email?: string | null; + name?: string | null; + username?: string | null; + }; + +/** + * Gets the display name for a user. + * Priority: email > name > username + * + * @param input - Either a user object or individual email, name, username properties + * @returns The display name string + */ +export function getUserDisplayName(input: UserDisplayNameInput): string { + let email: string | null | undefined; + let name: string | null | undefined; + let username: string | null | undefined; + + if ("user" in input) { + email = input.user.email; + name = input.user.name; + username = input.user.username; + } else { + email = input.email; + name = input.name; + username = input.username; + } + + return email || name || username || ""; +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index de2dc64a..d016ec77 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -340,6 +340,7 @@ export type ApprovalItem = { name: string | null; userId: string; username: string; + email: string | null; }; };