Merge branch 'dev' into feat/show-newt-install-command

This commit is contained in:
Fred KISSIE
2026-01-21 03:26:52 +01:00
71 changed files with 4144 additions and 628 deletions

View File

@@ -1,12 +1,14 @@
import { ApprovalFeed } from "@app/components/ApprovalFeed";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import ApprovalsBanner from "@app/components/ApprovalsBanner";
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
import type { ApprovalItem } from "@app/lib/queries";
import OrgProvider from "@app/providers/OrgProvider";
import type { GetOrgResponse } from "@server/routers/org";
import type { ListRolesResponse } from "@server/routers/role";
import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server";
@@ -35,6 +37,21 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
org = orgRes.data.data;
}
// Fetch roles to check if approvals are enabled
let hasApprovalsEnabled = false;
const rolesRes = await internal
.get<AxiosResponse<ListRolesResponse>>(
`/org/${params.orgId}/roles`,
await authCookieHeader()
)
.catch((e) => {});
if (rolesRes && rolesRes.status === 200) {
hasApprovalsEnabled = rolesRes.data.data.roles.some(
(role) => role.requireDeviceApproval === true
);
}
const t = await getTranslations();
return (
@@ -44,11 +61,16 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
description={t("accessApprovalsDescription")}
/>
<ApprovalsBanner />
<PaidFeaturesAlert />
<OrgProvider org={org}>
<div className="container mx-auto max-w-12xl">
<ApprovalFeed orgId={params.orgId} />
<ApprovalFeed
orgId={params.orgId}
hasApprovalsEnabled={hasApprovalsEnabled}
/>
</div>
</OrgProvider>
</>

View File

@@ -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,

View File

@@ -22,12 +22,13 @@ import {
import { Badge } from "@app/components/ui/badge";
import { Button } from "@app/components/ui/button";
import ActionBanner from "@app/components/ActionBanner";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { toast } from "@app/hooks/useToast";
import { useRouter } from "next/navigation";
import { useState, useEffect, useTransition } from "react";
import { Check, Ban, Shield, ShieldOff, Clock } from "lucide-react";
import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react";
import { useParams } from "next/navigation";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si";
@@ -111,18 +112,12 @@ function getPlatformFieldConfig(
kernelVersion: { show: false, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" },
serialNumber: { show: true, labelKey: "serialNumber" },
username: { show: true, labelKey: "username" },
hostname: { show: true, labelKey: "hostname" }
},
android: {
osVersion: { show: true, labelKey: "androidVersion" },
kernelVersion: { show: true, labelKey: "kernelVersion" },
arch: { show: true, labelKey: "architecture" },
deviceModel: { show: true, labelKey: "deviceModel" },
serialNumber: { show: true, labelKey: "serialNumber" },
username: { show: true, labelKey: "username" },
hostname: { show: true, labelKey: "hostname" }
},
unknown: {
osVersion: { show: true, labelKey: "osVersion" },
@@ -138,6 +133,7 @@ function getPlatformFieldConfig(
return configs[normalizedPlatform] || configs.unknown;
}
export default function GeneralPage() {
const { client, updateClient } = useClientContext();
const { isPaidUser } = usePaidStatus();
@@ -152,6 +148,20 @@ export default function GeneralPage() {
const showApprovalFeatures = build !== "oss" && isPaidUser;
const formatPostureValue = (value: boolean | null | undefined) => {
if (value === null || value === undefined) return "-";
return (
<div className="flex items-center gap-2">
{value ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
<span>{value ? t("enabled") : t("disabled")}</span>
</div>
);
};
// Fetch approval ID for this client if pending
useEffect(() => {
if (
@@ -407,13 +417,13 @@ export default function GeneralPage() {
)}
{client.fingerprint.osVersion &&
fieldConfig.osVersion.show && (
fieldConfig.osVersion?.show && (
<InfoSection>
<InfoSectionTitle>
{t(
fieldConfig
.osVersion
.labelKey
?.labelKey || "osVersion"
)}
</InfoSectionTitle>
<InfoSectionContent>
@@ -426,7 +436,7 @@ export default function GeneralPage() {
)}
{client.fingerprint.kernelVersion &&
fieldConfig.kernelVersion.show && (
fieldConfig.kernelVersion?.show && (
<InfoSection>
<InfoSectionTitle>
{t("kernelVersion")}
@@ -456,7 +466,7 @@ export default function GeneralPage() {
)}
{client.fingerprint.deviceModel &&
fieldConfig.deviceModel.show && (
fieldConfig.deviceModel?.show && (
<InfoSection>
<InfoSectionTitle>
{t("deviceModel")}
@@ -486,7 +496,7 @@ export default function GeneralPage() {
)}
{client.fingerprint.username &&
fieldConfig.username.show && (
fieldConfig.username?.show && (
<InfoSection>
<InfoSectionTitle>
{t("username")}
@@ -501,7 +511,7 @@ export default function GeneralPage() {
)}
{client.fingerprint.hostname &&
fieldConfig.hostname.show && (
fieldConfig.hostname?.show && (
<InfoSection>
<InfoSectionTitle>
{t("hostname")}
@@ -548,6 +558,218 @@ export default function GeneralPage() {
</SettingsSectionBody>
</SettingsSection>
)}
{/* Device Security Section */}
{build !== "oss" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("deviceSecurity")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("deviceSecurityDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{client.posture && Object.keys(client.posture).length > 0 ? (
<>
{!isPaidUser && <PaidFeaturesAlert />}
<InfoSections cols={3}>
{client.posture.biometricsEnabled !== null &&
client.posture.biometricsEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("biometricsEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.biometricsEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.diskEncrypted !== null &&
client.posture.diskEncrypted !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("diskEncrypted")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.diskEncrypted
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.firewallEnabled !== null &&
client.posture.firewallEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("firewallEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.firewallEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.autoUpdatesEnabled !== null &&
client.posture.autoUpdatesEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("autoUpdatesEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.autoUpdatesEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.tpmAvailable !== null &&
client.posture.tpmAvailable !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("tpmAvailable")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.tpmAvailable
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.windowsDefenderEnabled !== null &&
client.posture.windowsDefenderEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("windowsDefenderEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.windowsDefenderEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosSipEnabled !== null &&
client.posture.macosSipEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosSipEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture.macosSipEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosGatekeeperEnabled !== null &&
client.posture.macosGatekeeperEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosGatekeeperEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosGatekeeperEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.macosFirewallStealthMode !== null &&
client.posture.macosFirewallStealthMode !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("macosFirewallStealthMode")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.macosFirewallStealthMode
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.linuxAppArmorEnabled !== null &&
client.posture.linuxAppArmorEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxAppArmorEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxAppArmorEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
{client.posture.linuxSELinuxEnabled !== null &&
client.posture.linuxSELinuxEnabled !==
undefined && (
<InfoSection>
<InfoSectionTitle>
{t("linuxSELinuxEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.linuxSELinuxEnabled
)
: "-"}
</InfoSectionContent>
</InfoSection>
)}
</InfoSections>
</>
) : (
<div className="text-muted-foreground">
{t("noData")}
</div>
)}
</SettingsSectionBody>
</SettingsSection>
)}
</SettingsContainer>
);
}

View File

@@ -11,6 +11,7 @@ import {
GetLoginPageResponse
} from "@server/routers/loginPage/types";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
export interface AuthPageProps {
params: Promise<{ orgId: string }>;
@@ -18,6 +19,12 @@ export interface AuthPageProps {
export default async function AuthPage(props: AuthPageProps) {
const orgId = (await props.params).orgId;
// custom auth branding is only available in enterprise and saas
if (build === "oss") {
redirect(`/${orgId}/settings/general/`);
}
let subscriptionStatus: GetOrgTierResponse | null = null;
try {
const subRes = await getCachedSubscription(orgId);

View File

@@ -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})` : ""}`
}))
);

View File

@@ -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 (
<>
<SettingsSectionTitle
title={`${user?.email || user?.name || user?.username}`}
title={
user
? getUserDisplayName({
email: user.email,
name: user.name,
username: user.username
})
: ""
}
description={t("userDescription2")}
/>
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>

View File

@@ -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 (
<DeviceLoginForm

View File

@@ -61,15 +61,13 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
{
title: "sidebarClientResources",
href: "/{orgId}/settings/resources/client",
icon: <GlobeLock className="size-4 flex-none" />,
isBeta: true
icon: <GlobeLock className="size-4 flex-none" />
}
]
},
{
title: "sidebarClients",
icon: <MonitorUp className="size-4 flex-none" />,
isBeta: true,
items: [
{
href: "/{orgId}/settings/clients/user",