fix(i18n): localize Cancel button text across 25 modal components
Replaces all hardcoded "Cancel" button text with {t("cancel")} using the
common namespace in every modal component, plus a few adjacent button
texts. Adds import { useTranslations } + useTranslations("common") to
each component that needed it.
Files covered: access-tokens, jobs, networks, peer, posture-checks,
remote-access/ssh, reverse-proxy/auth, reverse-proxy/clusters,
reverse-proxy/domain, routes, settings, setup-keys, users — all modal
dialogs with Cancel buttons now respect the active locale.
This commit is contained in:
@@ -18,13 +18,13 @@ import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
Ban,
|
||||
GalleryHorizontalEnd,
|
||||
History,
|
||||
KeyRoundIcon,
|
||||
Mail,
|
||||
MonitorSmartphoneIcon,
|
||||
User2,
|
||||
Ban,
|
||||
GalleryHorizontalEnd,
|
||||
History,
|
||||
KeyRoundIcon,
|
||||
Mail,
|
||||
MonitorSmartphoneIcon,
|
||||
User2,
|
||||
} from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
@@ -47,372 +47,370 @@ import { UserPeersSection } from "@/modules/users/UserPeersSection";
|
||||
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
||||
|
||||
export default function UserPage() {
|
||||
const queryParameter = useSearchParams();
|
||||
const userId = queryParameter.get("id");
|
||||
const { permission } = usePermissions();
|
||||
const isServiceUser = queryParameter.get("service_user") === "true";
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
`/users?service_user=${isServiceUser}`,
|
||||
);
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
const queryParameter = useSearchParams();
|
||||
const userId = queryParameter.get("id");
|
||||
const { permission } = usePermissions();
|
||||
const isServiceUser = queryParameter.get("service_user") === "true";
|
||||
const { data: users, isLoading } = useFetchApi<User[]>(
|
||||
`/users?service_user=${isServiceUser}`,
|
||||
);
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
|
||||
const user = useMemo(() => {
|
||||
return users?.find((u) => u.id === userId);
|
||||
}, [users, userId]);
|
||||
const user = useMemo(() => {
|
||||
return users?.find((u) => u.id === userId);
|
||||
}, [users, userId]);
|
||||
|
||||
useRedirect("/team/users", false, !userId);
|
||||
useRedirect("/team/users", false, !userId);
|
||||
|
||||
const userGroups = useGroupIdsToGroups(user?.auto_groups);
|
||||
const userGroups = useGroupIdsToGroups(user?.auto_groups);
|
||||
|
||||
if (!permission.users.read) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<RestrictedAccess page={"User Information"} />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
if (!permission.users.read) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<RestrictedAccess page={"User Information"} />
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOwnerOrAdmin && user && !isLoading) {
|
||||
return <UserOverview user={user} initialGroups={[]} />;
|
||||
}
|
||||
if (!isOwnerOrAdmin && user && !isLoading) {
|
||||
return <UserOverview user={user} initialGroups={[]} />;
|
||||
}
|
||||
|
||||
if (isOwnerOrAdmin && user && !isLoading && userGroups) {
|
||||
return <UserOverview user={user} initialGroups={userGroups} />;
|
||||
}
|
||||
if (isOwnerOrAdmin && user && !isLoading && userGroups) {
|
||||
return <UserOverview user={user} initialGroups={userGroups} />;
|
||||
}
|
||||
|
||||
return <FullScreenLoading />;
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
initialGroups: Group[];
|
||||
user: User;
|
||||
initialGroups: Group[];
|
||||
};
|
||||
|
||||
function UserOverview({ user, initialGroups }: Readonly<Props>) {
|
||||
const t = useTranslations("users");
|
||||
const router = useRouter();
|
||||
const userRequest = useApiCall<User>("/users");
|
||||
const isServiceUser = !!user?.is_service_user;
|
||||
const { mutate } = useSWRConfig();
|
||||
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
||||
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("users");
|
||||
const router = useRouter();
|
||||
const userRequest = useApiCall<User>("/users");
|
||||
const isServiceUser = !!user?.is_service_user;
|
||||
const { mutate } = useSWRConfig();
|
||||
const { loggedInUser, isOwnerOrAdmin, isUser } = useLoggedInUser();
|
||||
const isLoggedInUser = loggedInUser ? loggedInUser?.id === user.id : false;
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: initialGroups,
|
||||
});
|
||||
const [selectedGroups, setSelectedGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: initialGroups,
|
||||
});
|
||||
|
||||
const [role, setRole] = useState(user.role || Role.User);
|
||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||
role,
|
||||
selectedGroups,
|
||||
]);
|
||||
const [role, setRole] = useState(user.role || Role.User);
|
||||
const { hasChanges, updateRef: updateChangesRef } = useHasChanges([
|
||||
role,
|
||||
selectedGroups,
|
||||
]);
|
||||
|
||||
const save = async () => {
|
||||
const groups = await saveGroups();
|
||||
const groupIds = groups.map((group) => group.id) as string[];
|
||||
const save = async () => {
|
||||
const groups = await saveGroups();
|
||||
const groupIds = groups.map((group) => group.id) as string[];
|
||||
|
||||
notify({
|
||||
title: user.name,
|
||||
description: "Changes successfully saved.",
|
||||
promise: userRequest
|
||||
.put(
|
||||
{
|
||||
role: role,
|
||||
auto_groups: groupIds,
|
||||
is_blocked: user.is_blocked,
|
||||
},
|
||||
`/${user.id}`,
|
||||
)
|
||||
.then(() => {
|
||||
mutate(`/users?service_user=${isServiceUser}`);
|
||||
updateChangesRef([role, selectedGroups]);
|
||||
}),
|
||||
loadingMessage: "Saving changes...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: user.name,
|
||||
description: "Changes successfully saved.",
|
||||
promise: userRequest
|
||||
.put(
|
||||
{
|
||||
role: role,
|
||||
auto_groups: groupIds,
|
||||
is_blocked: user.is_blocked,
|
||||
},
|
||||
`/${user.id}`,
|
||||
)
|
||||
.then(() => {
|
||||
mutate(`/users?service_user=${isServiceUser}`);
|
||||
updateChangesRef([role, selectedGroups]);
|
||||
}),
|
||||
loadingMessage: "Saving changes...",
|
||||
});
|
||||
};
|
||||
|
||||
const isProfilePage = !!user?.is_current && !isServiceUser;
|
||||
const canViewTokens = permission?.pats?.read;
|
||||
const canViewPeers = permission?.peers?.read;
|
||||
const isProfilePage = !!user?.is_current && !isServiceUser;
|
||||
const canViewTokens = permission?.pats?.read;
|
||||
const canViewPeers = permission?.peers?.read;
|
||||
|
||||
const showAccessTokens = (user?.is_current || isServiceUser) && canViewTokens;
|
||||
const showPeers = !isServiceUser && canViewPeers;
|
||||
const showTabs = isProfilePage && showPeers && showAccessTokens;
|
||||
const showSeparator = !showTabs;
|
||||
const showAccessTokens = (user?.is_current || isServiceUser) && canViewTokens;
|
||||
const showPeers = !isServiceUser && canViewPeers;
|
||||
const showTabs = isProfilePage && showPeers && showAccessTokens;
|
||||
const showSeparator = !showTabs;
|
||||
|
||||
const [tab, setTab] = useState(isServiceUser ? "access-tokens" : "peers");
|
||||
const [tab, setTab] = useState(isServiceUser ? "access-tokens" : "peers");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6 mb-4"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
disabled={!permission.users.read}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className={"p-default py-6 mb-4"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/team"}
|
||||
label={"Team"}
|
||||
disabled={!permission.users.read}
|
||||
icon={<TeamIcon size={13} />}
|
||||
/>
|
||||
|
||||
{isServiceUser ? (
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/service-users"}
|
||||
label={"Service Users"}
|
||||
icon={<IconSettings2 size={17} />}
|
||||
/>
|
||||
) : (
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={"Users"}
|
||||
disabled={!permission.users.read}
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
)}
|
||||
{isServiceUser ? (
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/service-users"}
|
||||
label={"Service Users"}
|
||||
icon={<IconSettings2 size={17} />}
|
||||
/>
|
||||
) : (
|
||||
<Breadcrumbs.Item
|
||||
href={"/team/users"}
|
||||
label={"Users"}
|
||||
disabled={!permission.users.read}
|
||||
icon={<User2 size={16} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Breadcrumbs.Item label={user.name || user.id} active />
|
||||
</Breadcrumbs>
|
||||
<Breadcrumbs.Item label={user.name || user.id} active />
|
||||
</Breadcrumbs>
|
||||
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<div
|
||||
className={
|
||||
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
|
||||
}
|
||||
style={
|
||||
isServiceUser
|
||||
? {
|
||||
color: "white",
|
||||
}
|
||||
: {
|
||||
color: user?.name
|
||||
? generateColorFromString(
|
||||
user?.name || user?.id || "System User",
|
||||
)
|
||||
: "#808080",
|
||||
}
|
||||
}
|
||||
>
|
||||
{isServiceUser ? (
|
||||
<IconSettings2 size={16} />
|
||||
) : (
|
||||
user?.name?.charAt(0) || user?.id?.charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<h1 className={"flex items-center gap-3"} title={user?.id}>
|
||||
{user.name || user.id}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{!isUser && (
|
||||
<div className={"flex gap-4"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={"w-full"}
|
||||
onClick={() => {
|
||||
isServiceUser
|
||||
? router.push("/team/service-users")
|
||||
: router.push("/team/users");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<div className={"flex justify-between max-w-6xl"}>
|
||||
<div>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<div
|
||||
className={
|
||||
"w-10 h-10 rounded-full relative flex items-center justify-center text-white uppercase text-md font-medium bg-nb-gray-900"
|
||||
}
|
||||
style={
|
||||
isServiceUser
|
||||
? {
|
||||
color: "white",
|
||||
}
|
||||
: {
|
||||
color: user?.name
|
||||
? generateColorFromString(
|
||||
user?.name || user?.id || "System User",
|
||||
)
|
||||
: "#808080",
|
||||
}
|
||||
}
|
||||
>
|
||||
{isServiceUser ? (
|
||||
<IconSettings2 size={16} />
|
||||
) : (
|
||||
user?.name?.charAt(0) || user?.id?.charAt(0)
|
||||
)}
|
||||
</div>
|
||||
<h1 className={"flex items-center gap-3"} title={user?.id}>
|
||||
{user.name || user.id}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
{!isUser && (
|
||||
<div className={"flex gap-4"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
className={"w-full"}
|
||||
onClick={() => {
|
||||
isServiceUser
|
||||
? router.push("/team/service-users")
|
||||
: router.push("/team/users");
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!hasChanges || !permission.users.update}
|
||||
onClick={save}
|
||||
data-cy={"save-changes"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!hasChanges || !permission.users.update}
|
||||
onClick={save}
|
||||
data-cy={"save-changes"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<UserInformationCard user={user} />
|
||||
<div className={"flex flex-col gap-8 w-1/2 "}>
|
||||
{!isServiceUser && isOwnerOrAdmin && (
|
||||
<div>
|
||||
<Label>Auto-assigned groups</Label>
|
||||
<HelpText>
|
||||
Groups will be assigned to peers added by this user.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
hideAllGroup={true}
|
||||
dataCy={"user-group-selector"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={"flex items-start"}>
|
||||
<div className={"w-2/3"}>
|
||||
<Label>User Role</Label>
|
||||
<HelpText>
|
||||
Set a role for the user to assign access permissions.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-1/3"}>
|
||||
<UserRoleSelector
|
||||
value={role}
|
||||
onChange={setRole}
|
||||
hideOwner={isServiceUser}
|
||||
currentUser={user}
|
||||
disabled={isLoggedInUser || !permission.users.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex gap-10 w-full mt-8 max-w-6xl items-start"}>
|
||||
<UserInformationCard user={user} />
|
||||
<div className={"flex flex-col gap-8 w-1/2 "}>
|
||||
{!isServiceUser && isOwnerOrAdmin && (
|
||||
<div>
|
||||
<Label>Auto-assigned groups</Label>
|
||||
<HelpText>
|
||||
Groups will be assigned to peers added by this user.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
disabled={isUser}
|
||||
onChange={setSelectedGroups}
|
||||
values={selectedGroups}
|
||||
hideAllGroup={true}
|
||||
dataCy={"user-group-selector"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={"flex items-start"}>
|
||||
<div className={"w-2/3"}>
|
||||
<Label>User Role</Label>
|
||||
<HelpText>
|
||||
Set a role for the user to assign access permissions.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-1/3"}>
|
||||
<UserRoleSelector
|
||||
value={role}
|
||||
onChange={setRole}
|
||||
hideOwner={isServiceUser}
|
||||
currentUser={user}
|
||||
disabled={isLoggedInUser || !permission.users.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showSeparator && <Separator />}
|
||||
{showSeparator && <Separator />}
|
||||
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={setTab}
|
||||
value={tab}
|
||||
className={"pb-0 mb-0"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"} hidden={!showTabs}>
|
||||
{showPeers && (
|
||||
<TabsTrigger value={"peers"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Peers
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsTrigger value={"access-tokens"}>
|
||||
<KeyRoundIcon size={16} />
|
||||
{t("accessTokens")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
{showPeers && (
|
||||
<TabsContent value={"peers"} className={"pb-8"}>
|
||||
<UserPeersSection user={user} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsContent value={"access-tokens"} className={"pb-8"}>
|
||||
<div className={"px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<h2>{t("accessTokens")}</h2>
|
||||
<Paragraph>
|
||||
{t("accessTokensDescription")}
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<CreateAccessTokenModal user={user}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
data-cy={"access-token-open-modal"}
|
||||
disabled={!permission.pats.create}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Create Access Token
|
||||
</Button>
|
||||
</CreateAccessTokenModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessTokensTable user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</PageContainer>
|
||||
);
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={setTab}
|
||||
value={tab}
|
||||
className={"pb-0 mb-0"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"} hidden={!showTabs}>
|
||||
{showPeers && (
|
||||
<TabsTrigger value={"peers"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Peers
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsTrigger value={"access-tokens"}>
|
||||
<KeyRoundIcon size={16} />
|
||||
{t("accessTokens")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
{showPeers && (
|
||||
<TabsContent value={"peers"} className={"pb-8"}>
|
||||
<UserPeersSection user={user} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{showAccessTokens && (
|
||||
<TabsContent value={"access-tokens"} className={"pb-8"}>
|
||||
<div className={"px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div>
|
||||
<h2>{t("accessTokens")}</h2>
|
||||
<Paragraph>{t("accessTokensDescription")}</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<CreateAccessTokenModal user={user}>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
data-cy={"access-token-open-modal"}
|
||||
disabled={!permission.pats.create}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Create Access Token
|
||||
</Button>
|
||||
</CreateAccessTokenModal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AccessTokensTable user={user} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function UserInformationCard({ user }: Readonly<{ user: User }>) {
|
||||
const isServiceUser = user.is_service_user || false;
|
||||
const neverLoggedIn = dayjs(user.last_login).isBefore(
|
||||
dayjs().subtract(1000, "years"),
|
||||
);
|
||||
const isPendingApproval = user?.pending_approval;
|
||||
const isServiceUser = user.is_service_user || false;
|
||||
const neverLoggedIn = dayjs(user.last_login).isBefore(
|
||||
dayjs().subtract(1000, "years"),
|
||||
);
|
||||
const isPendingApproval = user?.pending_approval;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<User2 size={16} />
|
||||
{user.name ? "Name" : "User ID"}
|
||||
</>
|
||||
}
|
||||
value={user.name || user.id}
|
||||
/>
|
||||
return (
|
||||
<Card>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<User2 size={16} />
|
||||
{user.name ? "Name" : "User ID"}
|
||||
</>
|
||||
}
|
||||
value={user.name || user.id}
|
||||
/>
|
||||
|
||||
{!isServiceUser && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Mail size={16} />
|
||||
E-Mail
|
||||
</>
|
||||
}
|
||||
value={user.email || "-"}
|
||||
/>
|
||||
)}
|
||||
{!isServiceUser && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Mail size={16} />
|
||||
E-Mail
|
||||
</>
|
||||
}
|
||||
value={user.email || "-"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<GalleryHorizontalEnd size={16} />
|
||||
Status
|
||||
</>
|
||||
}
|
||||
value={<UserStatusCell user={user} />}
|
||||
/>
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<GalleryHorizontalEnd size={16} />
|
||||
Status
|
||||
</>
|
||||
}
|
||||
value={<UserStatusCell user={user} />}
|
||||
/>
|
||||
|
||||
{!isServiceUser && (
|
||||
<>
|
||||
{!user.is_current &&
|
||||
user.role != Role.Owner &&
|
||||
!isPendingApproval && (
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
)}
|
||||
{!isServiceUser && (
|
||||
<>
|
||||
{!user.is_current &&
|
||||
user.role != Role.Owner &&
|
||||
!isPendingApproval && (
|
||||
<Card.ListItem
|
||||
tooltip={false}
|
||||
label={
|
||||
<>
|
||||
<Ban size={16} />
|
||||
Block User
|
||||
</>
|
||||
}
|
||||
value={<UserBlockCell user={user} isUserPage={true} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last login
|
||||
</>
|
||||
}
|
||||
value={
|
||||
neverLoggedIn
|
||||
? "Never"
|
||||
: dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(user.last_login) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
);
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last login
|
||||
</>
|
||||
}
|
||||
value={
|
||||
neverLoggedIn
|
||||
? "Never"
|
||||
: dayjs(user.last_login).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(user.last_login) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,40 +12,40 @@ import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
const config = loadConfig();
|
||||
|
||||
export const SessionLost = () => {
|
||||
const t = useTranslations("auth");
|
||||
const router = useRouter();
|
||||
const { logout } = useOidc();
|
||||
const t = useTranslations("auth");
|
||||
const router = useRouter();
|
||||
const { logout } = useOidc();
|
||||
|
||||
useEffect(() => {
|
||||
router.push("/peers");
|
||||
});
|
||||
useEffect(() => {
|
||||
router.push("/peers");
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-center flex-col h-screen max-w-md mx-auto"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"bg-nb-gray-930 mb-3 border border-nb-gray-900 h-10 w-10 rounded-md flex items-center justify-center "
|
||||
}
|
||||
>
|
||||
<NetBirdIcon size={20} />
|
||||
</div>
|
||||
<h1>{t("sessionExpired")}</h1>
|
||||
<Paragraph className={"text-center"}>
|
||||
{t("sessionExpiredDescription")}
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
className={"mt-5"}
|
||||
onClick={() => logout("", { client_id: config.clientId })}
|
||||
>
|
||||
{t("login")}
|
||||
<LogIn size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-center flex-col h-screen max-w-md mx-auto"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"bg-nb-gray-930 mb-3 border border-nb-gray-900 h-10 w-10 rounded-md flex items-center justify-center "
|
||||
}
|
||||
>
|
||||
<NetBirdIcon size={20} />
|
||||
</div>
|
||||
<h1>{t("sessionExpired")}</h1>
|
||||
<Paragraph className={"text-center"}>
|
||||
{t("sessionExpiredDescription")}
|
||||
</Paragraph>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
size={"sm"}
|
||||
className={"mt-5"}
|
||||
onClick={() => logout("", { client_id: config.clientId })}
|
||||
>
|
||||
{t("login")}
|
||||
<LogIn size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useSWRConfig } from "swr";
|
||||
import useCopyToClipboard from "@/hooks/useCopyToClipboard";
|
||||
import { AccessToken } from "@/interfaces/AccessToken";
|
||||
import { User } from "@/interfaces/User";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -41,6 +42,7 @@ export default function CreateAccessTokenModal({
|
||||
children,
|
||||
user,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [modal, setModal] = useState(false);
|
||||
const [successModal, setSuccessModal] = useState(false);
|
||||
const [token, setToken] = useState<string>("");
|
||||
@@ -132,6 +134,7 @@ export function AccessTokenModalContent({
|
||||
onSuccess,
|
||||
user,
|
||||
}: Readonly<ModalProps>) {
|
||||
const t = useTranslations("common");
|
||||
const tokenRequest = useApiCall<AccessToken>(`/users/${user.id}/tokens`);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -221,7 +224,7 @@ export function AccessTokenModalContent({
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -22,6 +22,7 @@ import { notify } from "@/components/Notification";
|
||||
import Separator from "@/components/Separator";
|
||||
import { Workload } from "@/interfaces/Job";
|
||||
import { useApiCall } from "@/utils/api";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
peerID: string;
|
||||
@@ -29,6 +30,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) {
|
||||
const t = useTranslations("common");
|
||||
const jobRequest = useApiCall<Workload>(`/peers/${peerID}/jobs`, true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -177,7 +179,7 @@ export function CreateDebugJobModalContent({ peerID, onSuccess }: Props) {
|
||||
<ModalFooter className="items-center">
|
||||
<div className="flex gap-3 w-full justify-end">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -15,6 +15,7 @@ import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
import { FolderGit2 } from "lucide-react";
|
||||
import Separator from "@components/Separator";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type ResourceGroupModalProps = {
|
||||
resource?: NetworkResource;
|
||||
@@ -55,6 +56,7 @@ const ResourceGroupModalContent = ({
|
||||
network,
|
||||
onUpdated,
|
||||
}: ModalProps) => {
|
||||
const t = useTranslations("common");
|
||||
const update = useApiCall<NetworkResource>(
|
||||
`/networks/${network?.id}/resources/${resource?.id}`,
|
||||
).put;
|
||||
@@ -106,7 +108,7 @@ const ResourceGroupModalContent = ({
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button variant={"primary"} onClick={updateResource}>
|
||||
|
||||
@@ -45,6 +45,7 @@ import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { RoutingPeerMasqueradeSwitch } from "@/modules/networks/routing-peers/RoutingPeerMasqueradeSwitch";
|
||||
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
network: Network;
|
||||
@@ -63,6 +64,7 @@ export default function NetworkRoutingPeerModal({
|
||||
onUpdated,
|
||||
router,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
return (
|
||||
<Modal open={open} onOpenChange={setOpen}>
|
||||
<RoutingPeerModalContent
|
||||
@@ -89,6 +91,7 @@ function RoutingPeerModalContent({
|
||||
onCreated,
|
||||
onUpdated,
|
||||
}: ContentProps) {
|
||||
const t = useTranslations("common");
|
||||
const isRoutingPeer = router ? router.peer != "" : true;
|
||||
|
||||
const [tab, setTab] = useState("router");
|
||||
@@ -363,7 +366,7 @@ function RoutingPeerModalContent({
|
||||
{tab == "router" && (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -1,178 +1,182 @@
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import {IconArrowRight} from "@tabler/icons-react";
|
||||
import {cn} from "@utils/helpers";
|
||||
import {HelpCircle} from "lucide-react";
|
||||
import { IconArrowRight } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import {useMemo} from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
|
||||
import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import {Intent} from "@/modules/onboarding/Onboarding";
|
||||
import { Intent } from "@/modules/onboarding/Onboarding";
|
||||
|
||||
type Props = {
|
||||
onSelect: (intent: Intent) => void,
|
||||
useCases?: string,
|
||||
isBusiness?: boolean
|
||||
type Props = {
|
||||
onSelect: (intent: Intent) => void;
|
||||
useCases?: string;
|
||||
isBusiness?: boolean;
|
||||
};
|
||||
|
||||
export const OnboardingIntent = ({onSelect, useCases, isBusiness}: Props) => {
|
||||
const t = useTranslations("onboarding");
|
||||
/**
|
||||
* Recommend Networks if users ticks any of these use cases
|
||||
*/
|
||||
const isNetworksRecommended = useMemo(() => {
|
||||
if (!useCases) return false;
|
||||
const intents = [
|
||||
"Zero Trust Security",
|
||||
"Employee Remote Access",
|
||||
"Business VPN",
|
||||
"Site-to-Site Connectivity",
|
||||
"IoT (Internet of Things)",
|
||||
"MSP (Managed Service Provider)",
|
||||
];
|
||||
for (const intent of intents) {
|
||||
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [useCases]);
|
||||
export const OnboardingIntent = ({ onSelect, useCases, isBusiness }: Props) => {
|
||||
const t = useTranslations("onboarding");
|
||||
/**
|
||||
* Recommend Networks if users ticks any of these use cases
|
||||
*/
|
||||
const isNetworksRecommended = useMemo(() => {
|
||||
if (!useCases) return false;
|
||||
const intents = [
|
||||
"Zero Trust Security",
|
||||
"Employee Remote Access",
|
||||
"Business VPN",
|
||||
"Site-to-Site Connectivity",
|
||||
"IoT (Internet of Things)",
|
||||
"MSP (Managed Service Provider)",
|
||||
];
|
||||
for (const intent of intents) {
|
||||
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [useCases]);
|
||||
|
||||
/**
|
||||
* Recommend P2P if users ticks any of these use cases
|
||||
*/
|
||||
const isP2PRecommended = useMemo(() => {
|
||||
if (!useCases) return false;
|
||||
const intents = [
|
||||
"Homelab Automation",
|
||||
"Home Remote Access",
|
||||
"File Access",
|
||||
"Gaming",
|
||||
];
|
||||
for (const intent of intents) {
|
||||
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [useCases]);
|
||||
/**
|
||||
* Recommend P2P if users ticks any of these use cases
|
||||
*/
|
||||
const isP2PRecommended = useMemo(() => {
|
||||
if (!useCases) return false;
|
||||
const intents = [
|
||||
"Homelab Automation",
|
||||
"Home Remote Access",
|
||||
"File Access",
|
||||
"Gaming",
|
||||
];
|
||||
for (const intent of intents) {
|
||||
if (useCases.toLowerCase().includes(intent.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [useCases]);
|
||||
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full justify-between"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center"}>{t("title")}</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
NetBird provides the flexibility of both a peer-to-peer overlay network and a remote network access
|
||||
solution.
|
||||
Choose what fits your needs, you can always combine both.
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 mt-8",
|
||||
"border border-nb-gray-900 rounded-lg flex items-start flex-col relative bg-nb-gray-930/60 transition-all ",
|
||||
)}
|
||||
>
|
||||
<IntentCard
|
||||
title={"Peer-to-Peer Network"}
|
||||
description={
|
||||
isBusiness ? "Install NetBird on two or more devices to create secure, direct WireGuard connections, like laptop to server or server to database. Add at least two machines to get started." :"Install NetBird on two or more devices in your homelab, such as your laptop, NAS, or Raspberry Pi, to create secure, direct WireGuard connections."
|
||||
}
|
||||
recommended={isP2PRecommended}
|
||||
icon={<PeerIcon size={18} className={"fill-netbird"}/>}
|
||||
onClick={() => onSelect(Intent.P2P)}
|
||||
/>
|
||||
<IntentCard
|
||||
title={"Remote Network Access"}
|
||||
description={
|
||||
isBusiness ? "Enable employee remote access to VMs, Kubernetes clusters, and cloud or on-prem resources without installing NetBird on every machine." : "Securely access your homelab remotely from anywhere without installing NetBird on every device."
|
||||
}
|
||||
recommended={isNetworksRecommended}
|
||||
icon={<NetworkRoutesIcon size={18} className={"fill-netbird"}/>}
|
||||
onClick={() => onSelect(Intent.NETWORKS)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full justify-between"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center"}>{t("title")}</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
NetBird provides the flexibility of both a peer-to-peer overlay
|
||||
network and a remote network access solution. Choose what fits your
|
||||
needs, you can always combine both.
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid grid-cols-1 mt-8",
|
||||
"border border-nb-gray-900 rounded-lg flex items-start flex-col relative bg-nb-gray-930/60 transition-all ",
|
||||
)}
|
||||
>
|
||||
<IntentCard
|
||||
title={"Peer-to-Peer Network"}
|
||||
description={
|
||||
isBusiness
|
||||
? "Install NetBird on two or more devices to create secure, direct WireGuard connections, like laptop to server or server to database. Add at least two machines to get started."
|
||||
: "Install NetBird on two or more devices in your homelab, such as your laptop, NAS, or Raspberry Pi, to create secure, direct WireGuard connections."
|
||||
}
|
||||
recommended={isP2PRecommended}
|
||||
icon={<PeerIcon size={18} className={"fill-netbird"} />}
|
||||
onClick={() => onSelect(Intent.P2P)}
|
||||
/>
|
||||
<IntentCard
|
||||
title={"Remote Network Access"}
|
||||
description={
|
||||
isBusiness
|
||||
? "Enable employee remote access to VMs, Kubernetes clusters, and cloud or on-prem resources without installing NetBird on every machine."
|
||||
: "Securely access your homelab remotely from anywhere without installing NetBird on every device."
|
||||
}
|
||||
recommended={isNetworksRecommended}
|
||||
icon={<NetworkRoutesIcon size={18} className={"fill-netbird"} />}
|
||||
onClick={() => onSelect(Intent.NETWORKS)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type IntentCardProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
recommended?: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
const IntentCard = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
onClick,
|
||||
recommended,
|
||||
}: IntentCardProps) => {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
"px-6 py-6 flex items-start flex-col relative hover:bg-nb-gray-920 transition-all group first:border-b border-nb-gray-900"
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={"flex gap-6"}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 flex items-center justify-center rounded-md shrink-0 mt-2",
|
||||
"bg-nb-gray-900 border border-nb-gray-800 ",
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={"flex gap-4 items-center"}>
|
||||
<div className={"text-left"}>
|
||||
<h2
|
||||
className={
|
||||
"text-base font-medium mb-.5 group-hover:text-netbird transition-all inline-flex gap-x-2 gap-y-1 flex-wrap"
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{recommended && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
Based on your previous choices, we recommend starting with{" "}
|
||||
{title}. You can always combine both options later.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative",
|
||||
"inline-flex text-[0.7rem] font-light bg-netbird/10 border border-netbird-400/30 text-netbird-400 rounded-full px-2 py-1 pb-0.5 leading-none",
|
||||
"hover:bg-netbird/20 cursor-help transition-all self-center",
|
||||
)}
|
||||
>
|
||||
Recommended
|
||||
<HelpCircle size={10} className={"ml-1"}/>
|
||||
</span>
|
||||
</FullTooltip>
|
||||
)}
|
||||
</h2>
|
||||
<p className={"!text-nb-gray-300 text-[.85rem]"}>{description}</p>
|
||||
</div>
|
||||
<div
|
||||
className={"h-full items-center text-nb-gray-400 hidden sm:flex"}
|
||||
>
|
||||
<IconArrowRight
|
||||
size={24}
|
||||
className={"shrink-0 group-hover:text-netbird"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
onClick,
|
||||
recommended,
|
||||
}: IntentCardProps) => {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
"px-6 py-6 flex items-start flex-col relative hover:bg-nb-gray-920 transition-all group first:border-b border-nb-gray-900"
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={"flex gap-6"}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-10 w-10 flex items-center justify-center rounded-md shrink-0 mt-2",
|
||||
"bg-nb-gray-900 border border-nb-gray-800 ",
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div className={"flex gap-4 items-center"}>
|
||||
<div className={"text-left"}>
|
||||
<h2
|
||||
className={
|
||||
"text-base font-medium mb-.5 group-hover:text-netbird transition-all inline-flex gap-x-2 gap-y-1 flex-wrap"
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{recommended && (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
Based on your previous choices, we recommend starting with{" "}
|
||||
{title}. You can always combine both options later.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"relative",
|
||||
"inline-flex text-[0.7rem] font-light bg-netbird/10 border border-netbird-400/30 text-netbird-400 rounded-full px-2 py-1 pb-0.5 leading-none",
|
||||
"hover:bg-netbird/20 cursor-help transition-all self-center",
|
||||
)}
|
||||
>
|
||||
Recommended
|
||||
<HelpCircle size={10} className={"ml-1"} />
|
||||
</span>
|
||||
</FullTooltip>
|
||||
)}
|
||||
</h2>
|
||||
<p className={"!text-nb-gray-300 text-[.85rem]"}>{description}</p>
|
||||
</div>
|
||||
<div
|
||||
className={"h-full items-center text-nb-gray-400 hidden sm:flex"}
|
||||
>
|
||||
<IconArrowRight
|
||||
size={24}
|
||||
className={"shrink-0 group-hover:text-netbird"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,248 +15,249 @@ import { Policy } from "@/interfaces/Policy";
|
||||
import { ResourceSingleAddressInput } from "@/modules/networks/resources/ResourceSingleAddressInput";
|
||||
|
||||
type Props = {
|
||||
onNetworkCreation?: (network: Network) => void;
|
||||
onResourceCreation?: (resource: NetworkResource) => void;
|
||||
onBack: () => void;
|
||||
onNetworkCreation?: (network: Network) => void;
|
||||
onResourceCreation?: (resource: NetworkResource) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const OnboardingAddResource = ({
|
||||
onNetworkCreation,
|
||||
onResourceCreation,
|
||||
onBack,
|
||||
onNetworkCreation,
|
||||
onResourceCreation,
|
||||
onBack,
|
||||
}: Props) => {
|
||||
const t = useTranslations("onboarding");
|
||||
const [resourceType, setResourceType] = useState("");
|
||||
const [resourceAddress, setResourceAddress] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [network, setNetwork] = useState<Network>();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { groups } = useGroups();
|
||||
const t = useTranslations("onboarding");
|
||||
const [resourceType, setResourceType] = useState("");
|
||||
const [resourceAddress, setResourceAddress] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [network, setNetwork] = useState<Network>();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { groups } = useGroups();
|
||||
|
||||
const networkRequest = useApiCall<Network>("/networks", true);
|
||||
const resourceRequest = useApiCall<NetworkResource>("/networks", true);
|
||||
const policyRequest = useApiCall<Policy>("/policies", true);
|
||||
const groupRequest = useApiCall<Group>("/groups", true);
|
||||
const networkRequest = useApiCall<Network>("/networks", true);
|
||||
const resourceRequest = useApiCall<NetworkResource>("/networks", true);
|
||||
const policyRequest = useApiCall<Policy>("/policies", true);
|
||||
const groupRequest = useApiCall<Group>("/groups", true);
|
||||
|
||||
const allGroupId = groups?.find((g) => g.name === "All")?.id;
|
||||
const allGroupId = groups?.find((g) => g.name === "All")?.id;
|
||||
|
||||
/**
|
||||
* Create a new network and add a resource to it
|
||||
*/
|
||||
const createResource = async () => {
|
||||
let myNetwork = network;
|
||||
/**
|
||||
* Create a new network and add a resource to it
|
||||
*/
|
||||
const createResource = async () => {
|
||||
let myNetwork = network;
|
||||
|
||||
if (!network) {
|
||||
await networkRequest
|
||||
.post({
|
||||
name: "My First Network",
|
||||
description: "Created during onboarding",
|
||||
})
|
||||
.then((n) => {
|
||||
myNetwork = n;
|
||||
onNetworkCreation?.(n);
|
||||
setNetwork(n);
|
||||
});
|
||||
}
|
||||
if (!network) {
|
||||
await networkRequest
|
||||
.post({
|
||||
name: "My First Network",
|
||||
description: "Created during onboarding",
|
||||
})
|
||||
.then((n) => {
|
||||
myNetwork = n;
|
||||
onNetworkCreation?.(n);
|
||||
setNetwork(n);
|
||||
});
|
||||
}
|
||||
|
||||
if (!myNetwork) return;
|
||||
if (!myNetwork) return;
|
||||
|
||||
notify({
|
||||
title: "My First Network",
|
||||
description: "Network & Resource created successfully",
|
||||
loadingMessage: "Creating your resource...",
|
||||
promise: resourceRequest
|
||||
.post(
|
||||
{
|
||||
name: resourceType === "subnet" ? "My Subnet" : "My Resource",
|
||||
description: "Created during onboarding",
|
||||
address: normalizeHostCIDR(resourceAddress),
|
||||
enabled: true,
|
||||
groups: [],
|
||||
},
|
||||
`/${myNetwork.id}/resources`,
|
||||
)
|
||||
.then((r) => {
|
||||
onResourceCreation?.(r);
|
||||
createOnboardingGroups().then(({ usersGroup, routingPeersGroup }) => {
|
||||
createUsersToResourcePolicy(r, usersGroup);
|
||||
createUsersToRoutingPeersPolicy(r, usersGroup, routingPeersGroup);
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "My First Network",
|
||||
description: "Network & Resource created successfully",
|
||||
loadingMessage: "Creating your resource...",
|
||||
promise: resourceRequest
|
||||
.post(
|
||||
{
|
||||
name: resourceType === "subnet" ? "My Subnet" : "My Resource",
|
||||
description: "Created during onboarding",
|
||||
address: normalizeHostCIDR(resourceAddress),
|
||||
enabled: true,
|
||||
groups: [],
|
||||
},
|
||||
`/${myNetwork.id}/resources`,
|
||||
)
|
||||
.then((r) => {
|
||||
onResourceCreation?.(r);
|
||||
createOnboardingGroups().then(({ usersGroup, routingPeersGroup }) => {
|
||||
createUsersToResourcePolicy(r, usersGroup);
|
||||
createUsersToRoutingPeersPolicy(r, usersGroup, routingPeersGroup);
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create Users and Routing Peers groups if they do not exist
|
||||
*/
|
||||
const createOnboardingGroups = async () => {
|
||||
let usersGroup = groups?.find((group) => group.name === "Users");
|
||||
let routingPeersGroup = groups?.find(
|
||||
(group) => group.name === "Routing Peers",
|
||||
);
|
||||
if (!usersGroup) {
|
||||
usersGroup = await groupRequest.post({
|
||||
name: "Users",
|
||||
});
|
||||
}
|
||||
if (!routingPeersGroup) {
|
||||
routingPeersGroup = await groupRequest.post({
|
||||
name: "Routing Peers",
|
||||
});
|
||||
}
|
||||
return {
|
||||
usersGroup,
|
||||
routingPeersGroup,
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Create Users and Routing Peers groups if they do not exist
|
||||
*/
|
||||
const createOnboardingGroups = async () => {
|
||||
let usersGroup = groups?.find((group) => group.name === "Users");
|
||||
let routingPeersGroup = groups?.find(
|
||||
(group) => group.name === "Routing Peers",
|
||||
);
|
||||
if (!usersGroup) {
|
||||
usersGroup = await groupRequest.post({
|
||||
name: "Users",
|
||||
});
|
||||
}
|
||||
if (!routingPeersGroup) {
|
||||
routingPeersGroup = await groupRequest.post({
|
||||
name: "Routing Peers",
|
||||
});
|
||||
}
|
||||
return {
|
||||
usersGroup,
|
||||
routingPeersGroup,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a policy that allows users to access the resource
|
||||
*/
|
||||
const createUsersToResourcePolicy = async (
|
||||
r: NetworkResource,
|
||||
usersGroup: Group,
|
||||
) => {
|
||||
const isSubnet = r.type === "subnet";
|
||||
/**
|
||||
* Create a policy that allows users to access the resource
|
||||
*/
|
||||
const createUsersToResourcePolicy = async (
|
||||
r: NetworkResource,
|
||||
usersGroup: Group,
|
||||
) => {
|
||||
const isSubnet = r.type === "subnet";
|
||||
|
||||
await policyRequest.post({
|
||||
name: `Users to ${r.name}`,
|
||||
description: `Allows access to this ${
|
||||
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
|
||||
}`,
|
||||
enabled: true,
|
||||
rules: [
|
||||
{
|
||||
name: `Users to ${r.name}`,
|
||||
description: `Allows access to this ${
|
||||
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
|
||||
}`,
|
||||
enabled: true,
|
||||
action: "accept",
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
sources: usersGroup ? [usersGroup.id] : [allGroupId],
|
||||
destinationResource: {
|
||||
type: r.type,
|
||||
id: r.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
await policyRequest.post({
|
||||
name: `Users to ${r.name}`,
|
||||
description: `Allows access to this ${
|
||||
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
|
||||
}`,
|
||||
enabled: true,
|
||||
rules: [
|
||||
{
|
||||
name: `Users to ${r.name}`,
|
||||
description: `Allows access to this ${
|
||||
isSubnet ? `subnet ${r.address}` : `resource ${r.address}`
|
||||
}`,
|
||||
enabled: true,
|
||||
action: "accept",
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
sources: usersGroup ? [usersGroup.id] : [allGroupId],
|
||||
destinationResource: {
|
||||
type: r.type,
|
||||
id: r.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a policy that allows users to access routing peers
|
||||
*/
|
||||
const createUsersToRoutingPeersPolicy = async (
|
||||
r: NetworkResource,
|
||||
usersGroup: Group,
|
||||
routingPeersGroup: Group,
|
||||
) => {
|
||||
await policyRequest
|
||||
.post({
|
||||
name: `Users to Routing Peers`,
|
||||
description: `Allows users to access routing peers`,
|
||||
enabled: true,
|
||||
rules: [
|
||||
{
|
||||
name: `Users to Routing Peers`,
|
||||
description: `Allows users to access routing peers`,
|
||||
enabled: true,
|
||||
action: "accept",
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
sources: usersGroup ? [usersGroup.id] : [allGroupId],
|
||||
destinations: routingPeersGroup
|
||||
? [routingPeersGroup.id]
|
||||
: [allGroupId],
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/policies");
|
||||
mutate("/groups");
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Create a policy that allows users to access routing peers
|
||||
*/
|
||||
const createUsersToRoutingPeersPolicy = async (
|
||||
r: NetworkResource,
|
||||
usersGroup: Group,
|
||||
routingPeersGroup: Group,
|
||||
) => {
|
||||
await policyRequest
|
||||
.post({
|
||||
name: `Users to Routing Peers`,
|
||||
description: `Allows users to access routing peers`,
|
||||
enabled: true,
|
||||
rules: [
|
||||
{
|
||||
name: `Users to Routing Peers`,
|
||||
description: `Allows users to access routing peers`,
|
||||
enabled: true,
|
||||
action: "accept",
|
||||
bidirectional: true,
|
||||
protocol: "all",
|
||||
sources: usersGroup ? [usersGroup.id] : [allGroupId],
|
||||
destinations: routingPeersGroup
|
||||
? [routingPeersGroup.id]
|
||||
: [allGroupId],
|
||||
},
|
||||
],
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/policies");
|
||||
mutate("/groups");
|
||||
});
|
||||
};
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (resourceType === "ip")
|
||||
return "Enter a single IPv4 or IPv6 address of your resource";
|
||||
if (resourceType === "subnet") return "Enter a CIDR range of your network";
|
||||
if (resourceType === "domain")
|
||||
return "Enter a domain name of your resource";
|
||||
}, [resourceType]);
|
||||
const description = useMemo(() => {
|
||||
if (resourceType === "ip")
|
||||
return "Enter a single IPv4 or IPv6 address of your resource";
|
||||
if (resourceType === "subnet") return "Enter a CIDR range of your network";
|
||||
if (resourceType === "domain")
|
||||
return "Enter a domain name of your resource";
|
||||
}, [resourceType]);
|
||||
|
||||
const placeholder = useMemo(() => {
|
||||
if (resourceType === "ip") return "e.g., 192.168.31.45 or 2001:db8::1";
|
||||
if (resourceType === "subnet") return "e.g., 192.168.1.0/24 or 2001:db8::/64";
|
||||
if (resourceType === "domain")
|
||||
return "e.g., service.internal or *.services.internal";
|
||||
}, [resourceType]);
|
||||
const placeholder = useMemo(() => {
|
||||
if (resourceType === "ip") return "e.g., 192.168.31.45 or 2001:db8::1";
|
||||
if (resourceType === "subnet")
|
||||
return "e.g., 192.168.1.0/24 or 2001:db8::/64";
|
||||
if (resourceType === "domain")
|
||||
return "e.g., service.internal or *.services.internal";
|
||||
}, [resourceType]);
|
||||
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full gap-4"}>
|
||||
<div className={"flex flex-col gap-8"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center"}>{t("addResource")}</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
Resources are your subnets, services, or machines inside your network.
|
||||
Pick the type you want to connect to.
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={"relative flex flex-col h-full gap-4"}>
|
||||
<div className={"flex flex-col gap-8"}>
|
||||
<div>
|
||||
<h1 className={"text-xl text-center"}>{t("addResource")}</h1>
|
||||
<div
|
||||
className={
|
||||
"text-sm text-nb-gray-300 font-light mt-2 block text-center sm:px-4"
|
||||
}
|
||||
>
|
||||
Resources are your subnets, services, or machines inside your
|
||||
network. Pick the type you want to connect to.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioCardGroup value={resourceType} onValueChange={setResourceType}>
|
||||
<RadioCard
|
||||
value={"ip"}
|
||||
title={"Single IP Address"}
|
||||
icon={<WorkflowIcon size={12} />}
|
||||
description={"IPv4 or IPv6 address like 192.168.31.45"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"subnet"}
|
||||
title={"Entire Subnet"}
|
||||
icon={<NetworkIcon size={12} />}
|
||||
description={"CIDR range like 192.168.0.0/24 or 2001:db8::/64"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"domain"}
|
||||
title={"Domain"}
|
||||
icon={<GlobeIcon size={12} />}
|
||||
description={
|
||||
"A domain like service.internal or a wildcard like *.services.internal"
|
||||
}
|
||||
/>
|
||||
</RadioCardGroup>
|
||||
<RadioCardGroup value={resourceType} onValueChange={setResourceType}>
|
||||
<RadioCard
|
||||
value={"ip"}
|
||||
title={"Single IP Address"}
|
||||
icon={<WorkflowIcon size={12} />}
|
||||
description={"IPv4 or IPv6 address like 192.168.31.45"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"subnet"}
|
||||
title={"Entire Subnet"}
|
||||
icon={<NetworkIcon size={12} />}
|
||||
description={"CIDR range like 192.168.0.0/24 or 2001:db8::/64"}
|
||||
/>
|
||||
<RadioCard
|
||||
value={"domain"}
|
||||
title={"Domain"}
|
||||
icon={<GlobeIcon size={12} />}
|
||||
description={
|
||||
"A domain like service.internal or a wildcard like *.services.internal"
|
||||
}
|
||||
/>
|
||||
</RadioCardGroup>
|
||||
|
||||
{resourceType && (
|
||||
<ResourceSingleAddressInput
|
||||
label={"What is the address of your resource?"}
|
||||
value={resourceAddress}
|
||||
onChange={setResourceAddress}
|
||||
onError={setError}
|
||||
description={description}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
{resourceType && (
|
||||
<ResourceSingleAddressInput
|
||||
label={"What is the address of your resource?"}
|
||||
value={resourceAddress}
|
||||
onChange={setResourceAddress}
|
||||
onError={setError}
|
||||
description={description}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={"flex gap-4"}>
|
||||
<Button variant={"secondary"} className={"w-full"} onClick={onBack}>
|
||||
Go Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={createResource}
|
||||
disabled={resourceAddress === "" || error !== ""}
|
||||
>
|
||||
Create Resource
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className={"flex gap-4"}>
|
||||
<Button variant={"secondary"} className={"w-full"} onClick={onBack}>
|
||||
Go Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={createResource}
|
||||
disabled={resourceAddress === "" || error !== ""}
|
||||
>
|
||||
Create Resource
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ import { SegmentedTabs } from "@components/SegmentedTabs";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { PeerSSHPolicyModal } from "@/modules/peer/PeerSSHPolicyModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open?: boolean;
|
||||
@@ -36,6 +37,7 @@ export const PeerSSHInstructions = ({
|
||||
onSuccess,
|
||||
peer,
|
||||
}: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [client, setClient] = useState("cli");
|
||||
const [policyModal, setPolicyModal] = useState(false);
|
||||
|
||||
@@ -136,7 +138,7 @@ export const PeerSSHInstructions = ({
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button variant={"primary"} onClick={onSuccess}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { GeoLocation, GeoLocationCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value?: GeoLocationCheck;
|
||||
@@ -69,6 +70,7 @@ export const PostureCheckGeoLocation = ({
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [allowDenyLocation, setAllowDenyLocation] = useState<string>(
|
||||
value?.action ? value.action : "allow",
|
||||
);
|
||||
@@ -190,7 +192,7 @@ const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useMemo, useState } from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { NetBirdVersionCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value?: NetBirdVersionCheck;
|
||||
@@ -54,6 +55,7 @@ export const PostureCheckNetBirdVersion = ({
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [version, setVersion] = useState(value?.min_version || "");
|
||||
|
||||
const versionError = useMemo(() => {
|
||||
@@ -111,7 +113,7 @@ const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
windowsKernelVersions,
|
||||
} from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value?: OperatingSystemVersionCheck;
|
||||
@@ -81,6 +82,7 @@ export const PostureCheckOperatingSystem = ({
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [tab] = useState(String(OperatingSystem.LINUX));
|
||||
|
||||
const firstTimeCheck = value === undefined;
|
||||
@@ -244,7 +246,7 @@ const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
disabled={!!versionError}
|
||||
|
||||
@@ -20,6 +20,7 @@ import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value?: PeerNetworkRangeCheck;
|
||||
@@ -67,6 +68,7 @@ interface NetworkRange {
|
||||
}
|
||||
|
||||
const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [allowOrDeny, setAllowOrDeny] = useState<string>(
|
||||
value?.action ? value.action : "allow",
|
||||
);
|
||||
@@ -205,7 +207,7 @@ const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -20,6 +20,7 @@ import AppleIcon from "@/assets/icons/AppleIcon";
|
||||
import WindowsIcon from "@/assets/icons/WindowsIcon";
|
||||
import { Process, ProcessCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
value?: ProcessCheck;
|
||||
@@ -58,6 +59,7 @@ export const PostureCheckProcess = ({ value, onChange, disabled }: Props) => {
|
||||
};
|
||||
|
||||
const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [processes, setProcesses] = useState<Process[]>(
|
||||
value?.processes
|
||||
? value.processes.map((p) => {
|
||||
@@ -288,7 +290,7 @@ const CheckContent = ({ value, onChange, disabled }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useMemo, useState } from "react";
|
||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { SSH_DOCS_LINK } from "@/modules/remote-access/ssh/useSSH";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -33,6 +34,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SSHCredentialsModal = ({ open, onOpenChange, peer }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const [username, setUsername] = useState(
|
||||
getOperatingSystem(peer.os) === OperatingSystem.WINDOWS
|
||||
? "Administrator"
|
||||
@@ -43,6 +45,7 @@ export const SSHCredentialsModal = ({ open, onOpenChange, peer }: Props) => {
|
||||
const [port, setPort] = useState(initialPort);
|
||||
|
||||
const userNameError = useMemo(() => {
|
||||
|
||||
if (username?.length === 0) return "Username cannot be empty";
|
||||
}, [username]);
|
||||
|
||||
@@ -136,7 +139,7 @@ export const SSHCredentialsModal = ({ open, onOpenChange, peer }: Props) => {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useReducer, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import type { HeaderAuthConfig } from "@/interfaces/ReverseProxy";
|
||||
|
||||
@@ -201,6 +202,7 @@ export default function AuthHeaderModal({
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [items, dispatch] = useReducer(
|
||||
headersReducer,
|
||||
currentHeaders,
|
||||
@@ -280,7 +282,7 @@ export default function AuthHeaderModal({
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -296,7 +298,7 @@ export default function AuthHeaderModal({
|
||||
<div />
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Group } from "@/interfaces/Group";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import Badge from "@components/Badge";
|
||||
import { CircleUser } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -26,6 +27,7 @@ export default function AuthNetBirdOnlyModal({
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const { users } = useUsers();
|
||||
const [groups, setGroups] = useState<Group[]>(currentGroups);
|
||||
const isEditing = isEnabled;
|
||||
@@ -74,7 +76,7 @@ export default function AuthNetBirdOnlyModal({
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -90,7 +92,7 @@ export default function AuthNetBirdOnlyModal({
|
||||
<div />
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Modal, ModalClose, ModalContent } from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import React, { useState } from "react";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -22,6 +23,7 @@ export default function AuthPasswordModal({
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [password, setPassword] = useState(currentPassword);
|
||||
const [isMasked, setIsMasked] = useState(isEnabled && currentPassword === "");
|
||||
const isEditing = isEnabled;
|
||||
@@ -82,7 +84,7 @@ export default function AuthPasswordModal({
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -98,7 +100,7 @@ export default function AuthPasswordModal({
|
||||
<div />
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -4,6 +4,7 @@ import ModalHeader from "@components/modal/ModalHeader";
|
||||
import PinCodeInput from "@components/PinCodeInput";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -22,6 +23,7 @@ export default function AuthPinModal({
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [pin, setPin] = useState(currentPin);
|
||||
const [isMasked, setIsMasked] = useState(isEnabled && currentPin === "");
|
||||
const isEditing = isEnabled;
|
||||
@@ -73,7 +75,7 @@ export default function AuthPinModal({
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
@@ -89,7 +91,7 @@ export default function AuthPinModal({
|
||||
<div />
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Group } from "@/interfaces/Group";
|
||||
import { useUsers } from "@/contexts/UsersProvider";
|
||||
import Badge from "@components/Badge";
|
||||
import { CircleUser } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -26,6 +27,7 @@ export default function AuthSSOModal({
|
||||
onSave,
|
||||
onRemove,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const { users } = useUsers();
|
||||
const [groups, setGroups] = useState<Group[]>(currentGroups);
|
||||
const isEditing = isEnabled;
|
||||
@@ -75,7 +77,7 @@ export default function AuthSSOModal({
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Save
|
||||
@@ -87,7 +89,7 @@ export default function AuthSSOModal({
|
||||
<div />
|
||||
<div className="flex gap-3">
|
||||
<ModalClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="secondary">{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button variant="primary" onClick={handleSave}>
|
||||
Add SSO
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
REVERSE_PROXY_SELFHOSTED_ROUTING_DOCS_LINK,
|
||||
ReverseProxyClusterToken,
|
||||
} from "@/interfaces/ReverseProxy";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -73,6 +74,7 @@ const renderHighlightedCommand = (command: string, highlights: string[]) => {
|
||||
};
|
||||
|
||||
export const ClustersModal = ({ open, onOpenChange }: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const { mutate } = useSWRConfig();
|
||||
const [tab, setTab] = useState("domain");
|
||||
const [domain, setDomain] = useState("");
|
||||
@@ -461,7 +463,7 @@ spec:
|
||||
{tab === "domain" && (
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -40,6 +41,7 @@ export const CustomDomainModal = ({
|
||||
onOpenChange,
|
||||
onDomainSubmit,
|
||||
}: Props) => {
|
||||
const t = useTranslations("common");
|
||||
const { domains } = useReverseProxies();
|
||||
const [domain, setDomain] = useState("");
|
||||
const [selectedCluster, setSelectedCluster] = useState("");
|
||||
@@ -178,7 +180,7 @@ export const CustomDomainModal = ({
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useRoutes } from "@/contexts/RoutesProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { GroupedRoute, Route } from "@/interfaces/Route";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
groupedRoute?: GroupedRoute;
|
||||
@@ -38,6 +39,7 @@ export default function RouteAddRoutingPeerModal({
|
||||
setModal,
|
||||
peer,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
return (
|
||||
<Modal open={modal} onOpenChange={setModal}>
|
||||
{modal && (
|
||||
@@ -58,6 +60,7 @@ type ModalProps = {
|
||||
};
|
||||
|
||||
function Content({ onSuccess, groupedRoute, peer }: ModalProps) {
|
||||
const t = useTranslations("common");
|
||||
const { createRoute } = useRoutes();
|
||||
|
||||
const [routingPeer, setRoutingPeer] = useState<Peer | undefined>(
|
||||
@@ -234,7 +237,7 @@ function Content({ onSuccess, groupedRoute, peer }: ModalProps) {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -58,6 +58,7 @@ import { Route } from "@/interfaces/Route";
|
||||
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { RoutingPeerMasqueradeSwitch } from "@/modules/networks/routing-peers/RoutingPeerMasqueradeSwitch";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@@ -72,6 +73,7 @@ export default function RouteModal({
|
||||
setOpen,
|
||||
distributionGroups,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
const { confirm } = useDialog();
|
||||
const router = useRouter();
|
||||
const [routePolicyModal, setRoutePolicyModal] = useState(false);
|
||||
@@ -158,6 +160,7 @@ export function RouteModalContent({
|
||||
isFirstExitNode = false,
|
||||
distributionGroups,
|
||||
}: ModalProps) {
|
||||
const t = useTranslations("common");
|
||||
const { createRoute } = useRoutes();
|
||||
const [tab, setTab] = useState(
|
||||
exitNode && peer ? "access-control" : "network",
|
||||
@@ -806,7 +809,7 @@ export function RouteModalContent({
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
{(tab == "network" || (tab == "access-control" && exitNode)) && (
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
)}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import { Peer } from "@/interfaces/Peer";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { RoutingPeerMasqueradeSwitch } from "@/modules/networks/routing-peers/RoutingPeerMasqueradeSwitch";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
route: Route;
|
||||
@@ -57,6 +58,7 @@ export default function RouteUpdateModal({
|
||||
onOpenChange,
|
||||
cell,
|
||||
}: Props) {
|
||||
const t = useTranslations("common");
|
||||
return (
|
||||
<>
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
@@ -79,6 +81,7 @@ type ModalProps = {
|
||||
};
|
||||
|
||||
function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
const t = useTranslations("common");
|
||||
const { updateRoute } = useRoutes();
|
||||
const { peers } = usePeers();
|
||||
const { groups: allGroups } = useGroups();
|
||||
@@ -528,7 +531,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -9,11 +9,11 @@ import { notify } from "@components/Notification";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/Select";
|
||||
import { useExpirationState } from "@hooks/useExpirationState";
|
||||
import { convertToSeconds } from "@hooks/useTimeFormatter";
|
||||
@@ -22,12 +22,12 @@ import { useApiCall } from "@utils/api";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
CalendarClock,
|
||||
ExternalLinkIcon,
|
||||
KeyRound,
|
||||
ShieldIcon,
|
||||
ShieldUserIcon,
|
||||
TimerResetIcon,
|
||||
CalendarClock,
|
||||
ExternalLinkIcon,
|
||||
KeyRound,
|
||||
ShieldIcon,
|
||||
ShieldUserIcon,
|
||||
TimerResetIcon,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -37,329 +37,327 @@ import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function AuthenticationTab({ account }: Readonly<Props>) {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
/**
|
||||
* Peer approval enabled
|
||||
*/
|
||||
const [peerApproval, setPeerApproval] = useState<boolean>(() => {
|
||||
try {
|
||||
return account?.settings?.extra?.peer_approval_enabled || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Peer approval enabled
|
||||
*/
|
||||
const [peerApproval, setPeerApproval] = useState<boolean>(() => {
|
||||
try {
|
||||
return account?.settings?.extra?.peer_approval_enabled || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* User approval required
|
||||
*/
|
||||
const [userApprovalRequired, setUserApprovalRequired] = useState<boolean>(
|
||||
() => {
|
||||
try {
|
||||
return account?.settings?.extra?.user_approval_required || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
/**
|
||||
* User approval required
|
||||
*/
|
||||
const [userApprovalRequired, setUserApprovalRequired] = useState<boolean>(
|
||||
() => {
|
||||
try {
|
||||
return account?.settings?.extra?.user_approval_required || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Local MFA (UI only, not wired to the backend yet)
|
||||
const [isLocalMFAEnabled, setIsLocalMFAEnabled] = useState<boolean>(() => {
|
||||
try {
|
||||
return account?.settings?.local_mfa_enabled || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
// Local MFA (UI only, not wired to the backend yet)
|
||||
const [isLocalMFAEnabled, setIsLocalMFAEnabled] = useState<boolean>(() => {
|
||||
try {
|
||||
return account?.settings?.local_mfa_enabled || false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Peer Expiration
|
||||
const [
|
||||
loginExpiration,
|
||||
setLoginExpiration,
|
||||
expiresIn,
|
||||
setExpiresIn,
|
||||
expireInterval,
|
||||
setExpireInterval,
|
||||
] = useExpirationState({
|
||||
enabled: account.settings.peer_login_expiration_enabled,
|
||||
expirationInSeconds: account.settings.peer_login_expiration || 86400,
|
||||
});
|
||||
// Peer Expiration
|
||||
const [
|
||||
loginExpiration,
|
||||
setLoginExpiration,
|
||||
expiresIn,
|
||||
setExpiresIn,
|
||||
expireInterval,
|
||||
setExpireInterval,
|
||||
] = useExpirationState({
|
||||
enabled: account.settings.peer_login_expiration_enabled,
|
||||
expirationInSeconds: account.settings.peer_login_expiration || 86400,
|
||||
});
|
||||
|
||||
// Peer Inactivity Expiration
|
||||
const [
|
||||
peerInactivityExpirationEnabled,
|
||||
setPeerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
] = useExpirationState({
|
||||
enabled: account.settings.peer_inactivity_expiration_enabled,
|
||||
expirationInSeconds: account.settings.peer_inactivity_expiration || 600,
|
||||
timeRange: ["minutes", "hours", "days"],
|
||||
});
|
||||
// Peer Inactivity Expiration
|
||||
const [
|
||||
peerInactivityExpirationEnabled,
|
||||
setPeerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
] = useExpirationState({
|
||||
enabled: account.settings.peer_inactivity_expiration_enabled,
|
||||
expirationInSeconds: account.settings.peer_inactivity_expiration || 600,
|
||||
timeRange: ["minutes", "hours", "days"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Save changes
|
||||
*/
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
/**
|
||||
* Save changes
|
||||
*/
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
peerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
isLocalMFAEnabled,
|
||||
]);
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
peerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
isLocalMFAEnabled,
|
||||
]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const expiration = convertToSeconds(expiresIn, expireInterval);
|
||||
const saveChanges = async () => {
|
||||
const expiration = convertToSeconds(expiresIn, expireInterval);
|
||||
|
||||
notify({
|
||||
title: "Save Authentication Settings",
|
||||
description: "Authentication settings successfully saved.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
peer_login_expiration_enabled: loginExpiration,
|
||||
peer_login_expiration: loginExpiration ? expiration : 86400,
|
||||
peer_inactivity_expiration_enabled: loginExpiration
|
||||
? peerInactivityExpirationEnabled
|
||||
: false,
|
||||
peer_inactivity_expiration: 600,
|
||||
extra: {
|
||||
...account.settings?.extra,
|
||||
peer_approval_enabled: peerApproval,
|
||||
user_approval_required: userApprovalRequired,
|
||||
},
|
||||
local_mfa_enabled: isLocalMFAEnabled
|
||||
},
|
||||
} as Account)
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
peerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Saving the authentication settings...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Save Authentication Settings",
|
||||
description: "Authentication settings successfully saved.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
peer_login_expiration_enabled: loginExpiration,
|
||||
peer_login_expiration: loginExpiration ? expiration : 86400,
|
||||
peer_inactivity_expiration_enabled: loginExpiration
|
||||
? peerInactivityExpirationEnabled
|
||||
: false,
|
||||
peer_inactivity_expiration: 600,
|
||||
extra: {
|
||||
...account.settings?.extra,
|
||||
peer_approval_enabled: peerApproval,
|
||||
user_approval_required: userApprovalRequired,
|
||||
},
|
||||
local_mfa_enabled: isLocalMFAEnabled,
|
||||
},
|
||||
} as Account)
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
peerApproval,
|
||||
userApprovalRequired,
|
||||
loginExpiration,
|
||||
expiresIn,
|
||||
expireInterval,
|
||||
peerInactivityExpirationEnabled,
|
||||
peerInactivityExpiresIn,
|
||||
peerInactivityExpireInterval,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Saving the authentication settings...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"authentication"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={"Settings"}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("authentication")}
|
||||
icon={<ShieldIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("authentication")}</h1>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/enforce-periodic-user-authentication"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Authentication
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"authentication"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={"Settings"}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("authentication")}
|
||||
icon={<ShieldIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("authentication")}</h1>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/enforce-periodic-user-authentication"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Authentication
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges || !permission.settings.update}
|
||||
onClick={saveChanges}
|
||||
data-cy={"save-authentication-settings"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges || !permission.settings.update}
|
||||
onClick={saveChanges}
|
||||
data-cy={"save-authentication-settings"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 w-full mt-8 mb-3"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={userApprovalRequired}
|
||||
onChange={setUserApprovalRequired}
|
||||
dataCy={"user-approval-required"}
|
||||
label={
|
||||
<>
|
||||
<ShieldUserIcon size={15} />
|
||||
User Approval Required
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Require manual approval for new users joining via <br />
|
||||
domain matching. Users will be blocked until approved.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
<div className={"flex flex-col gap-6 w-full mt-8 mb-3"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={userApprovalRequired}
|
||||
onChange={setUserApprovalRequired}
|
||||
dataCy={"user-approval-required"}
|
||||
label={
|
||||
<>
|
||||
<ShieldUserIcon size={15} />
|
||||
User Approval Required
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Require manual approval for new users joining via <br />
|
||||
domain matching. Users will be blocked until approved.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!account.settings.local_auth_disabled && account.settings.embedded_idp_enabled ?
|
||||
(
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={isLocalMFAEnabled}
|
||||
onChange={setIsLocalMFAEnabled}
|
||||
dataCy={"local-mfa-enabled"}
|
||||
label={
|
||||
<>
|
||||
<KeyRound size={15} />
|
||||
Enable Local MFA
|
||||
<SmallBadge
|
||||
text={"Beta"}
|
||||
variant={"sky"}
|
||||
className={"text-[9px] leading-none py-[3px] px-[5px]"}
|
||||
textClassName={"top-0"}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Require multi-factor authentication for users
|
||||
<br />
|
||||
authenticating with local credentials.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
{!account.settings.local_auth_disabled &&
|
||||
account.settings.embedded_idp_enabled ? (
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={isLocalMFAEnabled}
|
||||
onChange={setIsLocalMFAEnabled}
|
||||
dataCy={"local-mfa-enabled"}
|
||||
label={
|
||||
<>
|
||||
<KeyRound size={15} />
|
||||
Enable Local MFA
|
||||
<SmallBadge
|
||||
text={"Beta"}
|
||||
variant={"sky"}
|
||||
className={"text-[9px] leading-none py-[3px] px-[5px]"}
|
||||
textClassName={"top-0"}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Require multi-factor authentication for users
|
||||
<br />
|
||||
authenticating with local credentials.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={loginExpiration}
|
||||
onChange={(state) => {
|
||||
setLoginExpiration(state);
|
||||
!state && setPeerInactivityExpirationEnabled(false);
|
||||
}}
|
||||
dataCy={"peer-login-expiration"}
|
||||
label={
|
||||
<>
|
||||
<TimerResetIcon size={15} />
|
||||
Peer Session Expiration
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Request periodic re-authentication of peers <br />
|
||||
registered with SSO.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
|
||||
<div className={"flex flex-col"}>
|
||||
<FancyToggleSwitch
|
||||
value={loginExpiration}
|
||||
onChange={(state) => {
|
||||
setLoginExpiration(state);
|
||||
!state && setPeerInactivityExpirationEnabled(false);
|
||||
}}
|
||||
dataCy={"peer-login-expiration"}
|
||||
label={
|
||||
<>
|
||||
<TimerResetIcon size={15} />
|
||||
Peer Session Expiration
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Request periodic re-authentication of peers <br />
|
||||
registered with SSO.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
|
||||
!loginExpiration || !permission.settings.update
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "bg-nb-gray-930/80",
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex justify-between gap-10 mt-2")}>
|
||||
<div className={"w-full"}>
|
||||
<Label>Session Expiration</Label>
|
||||
<HelpText>
|
||||
Time after which every peer added with SSO login will
|
||||
require re-authentication.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full flex gap-3"}>
|
||||
<Input
|
||||
placeholder={"7"}
|
||||
maxWidthClass={"min-w-[100px]"}
|
||||
min={1}
|
||||
disabled={!loginExpiration || !permission.settings.update}
|
||||
data-cy={"peer-login-expiration-input"}
|
||||
max={180}
|
||||
className={"w-full"}
|
||||
value={expiresIn}
|
||||
type={"number"}
|
||||
onChange={(e) => setExpiresIn(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
disabled={!loginExpiration || !permission.settings.update}
|
||||
value={expireInterval}
|
||||
onValueChange={(v) => setExpireInterval(v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-cy={"peer-login-expiration-select"}
|
||||
>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<CalendarClock
|
||||
size={15}
|
||||
className={"text-nb-gray-300"}
|
||||
/>
|
||||
<SelectValue
|
||||
placeholder="Select interval..."
|
||||
data-cy={"peer-login-expiration-select-value"}
|
||||
/>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
data-cy={"peer-login-expiration-select-content"}
|
||||
>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
variant={"blank"}
|
||||
value={peerInactivityExpirationEnabled}
|
||||
onChange={setPeerInactivityExpirationEnabled}
|
||||
dataCy={"peer-inactivity-expiration"}
|
||||
label={<>Require login after disconnect</>}
|
||||
disabled={!permission.settings.update}
|
||||
helpText={
|
||||
<>
|
||||
Enable to require authentication after users disconnect from
|
||||
management for 10 minutes.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<div
|
||||
className={cn(
|
||||
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
|
||||
!loginExpiration || !permission.settings.update
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "bg-nb-gray-930/80",
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex justify-between gap-10 mt-2")}>
|
||||
<div className={"w-full"}>
|
||||
<Label>Session Expiration</Label>
|
||||
<HelpText>
|
||||
Time after which every peer added with SSO login will
|
||||
require re-authentication.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full flex gap-3"}>
|
||||
<Input
|
||||
placeholder={"7"}
|
||||
maxWidthClass={"min-w-[100px]"}
|
||||
min={1}
|
||||
disabled={!loginExpiration || !permission.settings.update}
|
||||
data-cy={"peer-login-expiration-input"}
|
||||
max={180}
|
||||
className={"w-full"}
|
||||
value={expiresIn}
|
||||
type={"number"}
|
||||
onChange={(e) => setExpiresIn(e.target.value)}
|
||||
/>
|
||||
<Select
|
||||
disabled={!loginExpiration || !permission.settings.update}
|
||||
value={expireInterval}
|
||||
onValueChange={(v) => setExpireInterval(v)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className="w-full"
|
||||
data-cy={"peer-login-expiration-select"}
|
||||
>
|
||||
<div className={"flex items-center gap-3"}>
|
||||
<CalendarClock
|
||||
size={15}
|
||||
className={"text-nb-gray-300"}
|
||||
/>
|
||||
<SelectValue
|
||||
placeholder="Select interval..."
|
||||
data-cy={"peer-login-expiration-select-value"}
|
||||
/>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
data-cy={"peer-login-expiration-select-content"}
|
||||
>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
variant={"blank"}
|
||||
value={peerInactivityExpirationEnabled}
|
||||
onChange={setPeerInactivityExpirationEnabled}
|
||||
dataCy={"peer-inactivity-expiration"}
|
||||
label={<>Require login after disconnect</>}
|
||||
disabled={!permission.settings.update}
|
||||
helpText={
|
||||
<>
|
||||
Enable to require authentication after users disconnect from
|
||||
management for 10 minutes.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Label } from "@components/Label";
|
||||
import { notify } from "@components/Notification";
|
||||
import { PeerGroupSelector } from "@components/PeerGroupSelector";
|
||||
import {
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
SelectDropdown,
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import { Callout } from "@components/Callout";
|
||||
import { useHasChanges } from "@hooks/useHasChanges";
|
||||
@@ -18,12 +18,12 @@ import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
ClockFadingIcon,
|
||||
ExternalLinkIcon,
|
||||
FlaskConicalIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
AlertTriangle,
|
||||
RefreshCcw,
|
||||
ClockFadingIcon,
|
||||
ExternalLinkIcon,
|
||||
FlaskConicalIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
AlertTriangle,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -37,376 +37,376 @@ import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { SkeletonSettings } from "@components/skeletons/SkeletonSettings";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
const latestOrCustomVersion = [
|
||||
{
|
||||
label: "Disabled",
|
||||
value: "disabled",
|
||||
},
|
||||
{
|
||||
label: "Latest Version",
|
||||
value: "latest",
|
||||
},
|
||||
{
|
||||
label: "Custom Version",
|
||||
value: "custom",
|
||||
},
|
||||
{
|
||||
label: "Disabled",
|
||||
value: "disabled",
|
||||
},
|
||||
{
|
||||
label: "Latest Version",
|
||||
value: "latest",
|
||||
},
|
||||
{
|
||||
label: "Custom Version",
|
||||
value: "custom",
|
||||
},
|
||||
] as SelectOption[];
|
||||
|
||||
export default function ClientSettingsTab({ account }: Readonly<Props>) {
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
|
||||
return isGroupsLoading ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<ClientSettingsTabContent account={account} />
|
||||
);
|
||||
return isGroupsLoading ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<ClientSettingsTabContent account={account} />
|
||||
);
|
||||
}
|
||||
|
||||
function ClientSettingsTabContent({ account }: Readonly<Props>) {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
|
||||
|
||||
const [lazyConnection, setLazyConnection] = useState(
|
||||
account.settings?.lazy_connection_enabled ?? false,
|
||||
);
|
||||
const [lazyConnection, setLazyConnection] = useState(
|
||||
account.settings?.lazy_connection_enabled ?? false,
|
||||
);
|
||||
|
||||
const autoUpdateSetting = account.settings?.auto_update_version;
|
||||
const isAutoUpdateEnabled =
|
||||
!!autoUpdateSetting && autoUpdateSetting !== "disabled";
|
||||
const isCustomVersion = validator.isValidVersion(autoUpdateSetting);
|
||||
const [autoUpdateMethod, setAutoUpdateMethod] = useState(
|
||||
isAutoUpdateEnabled ? (isCustomVersion ? "custom" : "latest") : "disabled",
|
||||
);
|
||||
const autoUpdateSetting = account.settings?.auto_update_version;
|
||||
const isAutoUpdateEnabled =
|
||||
!!autoUpdateSetting && autoUpdateSetting !== "disabled";
|
||||
const isCustomVersion = validator.isValidVersion(autoUpdateSetting);
|
||||
const [autoUpdateMethod, setAutoUpdateMethod] = useState(
|
||||
isAutoUpdateEnabled ? (isCustomVersion ? "custom" : "latest") : "disabled",
|
||||
);
|
||||
|
||||
const [autoUpdateCustomVersion, setAutoUpdateCustomVersion] = useState(
|
||||
isCustomVersion ? autoUpdateSetting : "",
|
||||
);
|
||||
const [autoUpdateCustomVersion, setAutoUpdateCustomVersion] = useState(
|
||||
isCustomVersion ? autoUpdateSetting : "",
|
||||
);
|
||||
|
||||
const [autoUpdateAlways, setAutoUpdateAlways] = useState(
|
||||
account.settings?.auto_update_always ?? false,
|
||||
);
|
||||
const [autoUpdateAlways, setAutoUpdateAlways] = useState(
|
||||
account.settings?.auto_update_always ?? false,
|
||||
);
|
||||
|
||||
const [peerExposeEnabled, setPeerExposeEnabled] = useState<boolean>(
|
||||
account?.settings?.peer_expose_enabled ?? false,
|
||||
);
|
||||
const [peerExposeGroups, setPeerExposeGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: account.settings?.peer_expose_groups,
|
||||
});
|
||||
const peerExposeGroupNames = useMemo(
|
||||
() => peerExposeGroups.map((g) => g.name).sort(),
|
||||
[peerExposeGroups],
|
||||
);
|
||||
const [peerExposeEnabled, setPeerExposeEnabled] = useState<boolean>(
|
||||
account?.settings?.peer_expose_enabled ?? false,
|
||||
);
|
||||
const [peerExposeGroups, setPeerExposeGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: account.settings?.peer_expose_groups,
|
||||
});
|
||||
const peerExposeGroupNames = useMemo(
|
||||
() => peerExposeGroups.map((g) => g.name).sort(),
|
||||
[peerExposeGroups],
|
||||
);
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
autoUpdateMethod,
|
||||
autoUpdateCustomVersion,
|
||||
autoUpdateAlways,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroupNames,
|
||||
]);
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
autoUpdateMethod,
|
||||
autoUpdateCustomVersion,
|
||||
autoUpdateAlways,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroupNames,
|
||||
]);
|
||||
|
||||
const handleUpdateMethodChange = (value: string) => {
|
||||
setAutoUpdateMethod(value);
|
||||
if (value === "disabled" || value === "latest") {
|
||||
setAutoUpdateCustomVersion("");
|
||||
}
|
||||
};
|
||||
const handleUpdateMethodChange = (value: string) => {
|
||||
setAutoUpdateMethod(value);
|
||||
if (value === "disabled" || value === "latest") {
|
||||
setAutoUpdateCustomVersion("");
|
||||
}
|
||||
};
|
||||
|
||||
const versionError = useMemo(() => {
|
||||
const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
|
||||
if (autoUpdateCustomVersion == "") return "";
|
||||
if (autoUpdateCustomVersion == "-") return "";
|
||||
const validSemver = validator.isValidVersion(autoUpdateCustomVersion);
|
||||
if (!validSemver) return msg;
|
||||
return "";
|
||||
}, [autoUpdateCustomVersion]);
|
||||
const versionError = useMemo(() => {
|
||||
const msg = "Please enter a valid version, e.g., 0.2, 0.2.0, 0.2.0-alpha.1";
|
||||
if (autoUpdateCustomVersion == "") return "";
|
||||
if (autoUpdateCustomVersion == "-") return "";
|
||||
const validSemver = validator.isValidVersion(autoUpdateCustomVersion);
|
||||
if (!validSemver) return msg;
|
||||
return "";
|
||||
}, [autoUpdateCustomVersion]);
|
||||
|
||||
const canSaveCustomVersion =
|
||||
autoUpdateCustomVersion !== "" &&
|
||||
autoUpdateMethod === "custom" &&
|
||||
versionError === "";
|
||||
const canSaveCustomVersion =
|
||||
autoUpdateCustomVersion !== "" &&
|
||||
autoUpdateMethod === "custom" &&
|
||||
versionError === "";
|
||||
|
||||
const isSaveButtonDisabled = useMemo(() => {
|
||||
return (
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
(autoUpdateMethod === "custom" && !canSaveCustomVersion) ||
|
||||
(peerExposeEnabled && peerExposeGroups.length === 0)
|
||||
);
|
||||
}, [
|
||||
hasChanges,
|
||||
permission.settings.update,
|
||||
autoUpdateMethod,
|
||||
canSaveCustomVersion,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroups,
|
||||
]);
|
||||
const isSaveButtonDisabled = useMemo(() => {
|
||||
return (
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
(autoUpdateMethod === "custom" && !canSaveCustomVersion) ||
|
||||
(peerExposeEnabled && peerExposeGroups.length === 0)
|
||||
);
|
||||
}, [
|
||||
hasChanges,
|
||||
permission.settings.update,
|
||||
autoUpdateMethod,
|
||||
canSaveCustomVersion,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroups,
|
||||
]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const groups = await saveGroups();
|
||||
const peerExposeGroupIds = groups
|
||||
.map((group) => group.id)
|
||||
.filter(Boolean) as string[];
|
||||
const saveChanges = async () => {
|
||||
const groups = await saveGroups();
|
||||
const peerExposeGroupIds = groups
|
||||
.map((group) => group.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
notify({
|
||||
title: "Client Settings",
|
||||
description: `Client settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
|
||||
auto_update_always: autoUpdateAlways,
|
||||
peer_expose_enabled: peerExposeEnabled,
|
||||
peer_expose_groups: peerExposeGroupIds,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
autoUpdateMethod,
|
||||
autoUpdateCustomVersion,
|
||||
autoUpdateAlways,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroupNames,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating client settings...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Client Settings",
|
||||
description: `Client settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
|
||||
auto_update_always: autoUpdateAlways,
|
||||
peer_expose_enabled: peerExposeEnabled,
|
||||
peer_expose_groups: peerExposeGroupIds,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
autoUpdateMethod,
|
||||
autoUpdateCustomVersion,
|
||||
autoUpdateAlways,
|
||||
peerExposeEnabled,
|
||||
peerExposeGroupNames,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating client settings...",
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLazyConnection = async (toggle: boolean) => {
|
||||
notify({
|
||||
title: "Lazy Connections",
|
||||
description: `Lazy Connections successfully ${
|
||||
toggle ? "enabled" : "disabled"
|
||||
}.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
lazy_connection_enabled: toggle,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setLazyConnection(toggle);
|
||||
mutate("/accounts");
|
||||
}),
|
||||
loadingMessage: "Updating Lazy Connections setting...",
|
||||
});
|
||||
};
|
||||
const toggleLazyConnection = async (toggle: boolean) => {
|
||||
notify({
|
||||
title: "Lazy Connections",
|
||||
description: `Lazy Connections successfully ${
|
||||
toggle ? "enabled" : "disabled"
|
||||
}.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
lazy_connection_enabled: toggle,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setLazyConnection(toggle);
|
||||
mutate("/accounts");
|
||||
}),
|
||||
loadingMessage: "Updating Lazy Connections setting...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"clients"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=clients"}
|
||||
label={t("clients")}
|
||||
icon={<MonitorSmartphoneIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("clients")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={isSaveButtonDisabled}
|
||||
onClick={saveChanges}
|
||||
data-cy={"save-clients-settings"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"clients"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=clients"}
|
||||
label={t("clients")}
|
||||
icon={<MonitorSmartphoneIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("clients")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={isSaveButtonDisabled}
|
||||
onClick={saveChanges}
|
||||
data-cy={"save-clients-settings"}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-10 w-full mt-8"}>
|
||||
<div className={"flex flex-col relative"}>
|
||||
<Label>
|
||||
<RefreshCcw size={15} />
|
||||
Automatic Updates
|
||||
<SmallBadge
|
||||
text={"Beta"}
|
||||
variant={"sky"}
|
||||
className={"text-[9px] leading-none py-[3px] px-[5px]"}
|
||||
textClassName={"top-0"}
|
||||
/>
|
||||
</Label>
|
||||
<HelpText>
|
||||
Configure how NetBird clients receive update notifications.
|
||||
When enabled, users will be prompted to install the selected
|
||||
version. This requires at least NetBird{" "}
|
||||
<span className={"text-white font-medium"}>v0.61.0</span>.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/peers/auto-update"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
<div className={"gap-4 items-center grid grid-cols-2"}>
|
||||
<SelectDropdown
|
||||
value={autoUpdateMethod}
|
||||
onChange={handleUpdateMethodChange}
|
||||
options={latestOrCustomVersion}
|
||||
/>
|
||||
<Input
|
||||
value={autoUpdateCustomVersion}
|
||||
customPrefix={"Version"}
|
||||
placeholder={"e.g., 0.52.2"}
|
||||
error={versionError}
|
||||
errorTooltip={true}
|
||||
disabled={autoUpdateMethod !== "custom"}
|
||||
onChange={(v) => {
|
||||
setAutoUpdateCustomVersion(v.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
className={"mt-4"}
|
||||
value={autoUpdateAlways}
|
||||
onChange={setAutoUpdateAlways}
|
||||
label={
|
||||
<>
|
||||
<AlertTriangle size={15} className={"text-yellow-400"} />
|
||||
Force Automatic Updates
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"When enabled, updates are installed automatically in the background without user interaction."
|
||||
}
|
||||
disabled={
|
||||
!permission.settings.update || autoUpdateMethod === "disabled"
|
||||
}
|
||||
/>
|
||||
{autoUpdateAlways && autoUpdateMethod !== "disabled" && (
|
||||
<Callout
|
||||
className={"mt-3"}
|
||||
variant={"warning"}
|
||||
icon={
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
className={"shrink-0 relative top-[3px]"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Enabling automatic updates will restart the NetBird client
|
||||
during updates, which can temporarily disrupt active
|
||||
connections. Use with caution in production environments.
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex flex-col gap-10 w-full mt-8"}>
|
||||
<div className={"flex flex-col relative"}>
|
||||
<Label>
|
||||
<RefreshCcw size={15} />
|
||||
Automatic Updates
|
||||
<SmallBadge
|
||||
text={"Beta"}
|
||||
variant={"sky"}
|
||||
className={"text-[9px] leading-none py-[3px] px-[5px]"}
|
||||
textClassName={"top-0"}
|
||||
/>
|
||||
</Label>
|
||||
<HelpText>
|
||||
Configure how NetBird clients receive update notifications. When
|
||||
enabled, users will be prompted to install the selected version.
|
||||
This requires at least NetBird{" "}
|
||||
<span className={"text-white font-medium"}>v0.61.0</span>.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/manage/peers/auto-update"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
<div className={"gap-4 items-center grid grid-cols-2"}>
|
||||
<SelectDropdown
|
||||
value={autoUpdateMethod}
|
||||
onChange={handleUpdateMethodChange}
|
||||
options={latestOrCustomVersion}
|
||||
/>
|
||||
<Input
|
||||
value={autoUpdateCustomVersion}
|
||||
customPrefix={"Version"}
|
||||
placeholder={"e.g., 0.52.2"}
|
||||
error={versionError}
|
||||
errorTooltip={true}
|
||||
disabled={autoUpdateMethod !== "custom"}
|
||||
onChange={(v) => {
|
||||
setAutoUpdateCustomVersion(v.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FancyToggleSwitch
|
||||
className={"mt-4"}
|
||||
value={autoUpdateAlways}
|
||||
onChange={setAutoUpdateAlways}
|
||||
label={
|
||||
<>
|
||||
<AlertTriangle size={15} className={"text-yellow-400"} />
|
||||
Force Automatic Updates
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"When enabled, updates are installed automatically in the background without user interaction."
|
||||
}
|
||||
disabled={
|
||||
!permission.settings.update || autoUpdateMethod === "disabled"
|
||||
}
|
||||
/>
|
||||
{autoUpdateAlways && autoUpdateMethod !== "disabled" && (
|
||||
<Callout
|
||||
className={"mt-3"}
|
||||
variant={"warning"}
|
||||
icon={
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
className={"shrink-0 relative top-[3px]"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Enabling automatic updates will restart the NetBird client
|
||||
during updates, which can temporarily disrupt active
|
||||
connections. Use with caution in production environments.
|
||||
</Callout>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<Label>
|
||||
<ReverseProxyIcon size={15} className={"fill-nb-gray-300"} />
|
||||
Expose Services from CLI
|
||||
</Label>
|
||||
<HelpText>
|
||||
Allow peers to expose local services through the NetBird reverse
|
||||
proxy using the CLI. <br /> This requires at least NetBird{" "}
|
||||
<span className={"text-white font-medium"}>v0.66.0</span>.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/manage/reverse-proxy/expose-from-cli"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Label>
|
||||
<ReverseProxyIcon size={15} className={"fill-nb-gray-300"} />
|
||||
Expose Services from CLI
|
||||
</Label>
|
||||
<HelpText>
|
||||
Allow peers to expose local services through the NetBird reverse
|
||||
proxy using the CLI. <br /> This requires at least NetBird{" "}
|
||||
<span className={"text-white font-medium"}>v0.66.0</span>.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/manage/reverse-proxy/expose-from-cli"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
</div>
|
||||
|
||||
<FancyToggleSwitch
|
||||
className={"mt-2"}
|
||||
value={peerExposeEnabled}
|
||||
onChange={setPeerExposeEnabled}
|
||||
label={"Enable Peer Expose"}
|
||||
helpText={
|
||||
"When enabled, peers can expose local HTTP services accessible via a public URL."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
className={"mt-2"}
|
||||
value={peerExposeEnabled}
|
||||
onChange={setPeerExposeEnabled}
|
||||
label={"Enable Peer Expose"}
|
||||
helpText={
|
||||
"When enabled, peers can expose local HTTP services accessible via a public URL."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
|
||||
!peerExposeEnabled
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "bg-nb-gray-930/80",
|
||||
)}
|
||||
>
|
||||
<div className={"mt-2"}>
|
||||
<Label>Allowed peer groups</Label>
|
||||
<HelpText>
|
||||
Select which peer groups are allowed to expose services. At
|
||||
least one group is required.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
values={peerExposeGroups}
|
||||
onChange={setPeerExposeGroups}
|
||||
placeholder="Select peer groups..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"border border-nb-gray-900 border-t-0 rounded-b-md bg-nb-gray-940 px-[1.28rem] pt-3 pb-5 flex flex-col gap-4 mx-[0.25rem]",
|
||||
!peerExposeEnabled
|
||||
? "opacity-50 pointer-events-none"
|
||||
: "bg-nb-gray-930/80",
|
||||
)}
|
||||
>
|
||||
<div className={"mt-2"}>
|
||||
<Label>Allowed peer groups</Label>
|
||||
<HelpText>
|
||||
Select which peer groups are allowed to expose services. At
|
||||
least one group is required.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
values={peerExposeGroups}
|
||||
onChange={setPeerExposeGroups}
|
||||
placeholder="Select peer groups..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>
|
||||
<FlaskConicalIcon size={15} />
|
||||
Experimental
|
||||
</Label>
|
||||
<div>
|
||||
<Label>
|
||||
<FlaskConicalIcon size={15} />
|
||||
Experimental
|
||||
</Label>
|
||||
|
||||
<HelpText>
|
||||
Lazy connections are an experimental feature. Functionality and
|
||||
behavior may evolve. Instead of maintaining always-on connections,
|
||||
NetBird activates them on-demand based on activity or signaling.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/lazy-connection"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
<FancyToggleSwitch
|
||||
className={"mt-2"}
|
||||
value={lazyConnection}
|
||||
onChange={toggleLazyConnection}
|
||||
label={
|
||||
<>
|
||||
<ClockFadingIcon size={15} />
|
||||
Enable Lazy Connections
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Allow to establish connections between peers only when
|
||||
required. This requires NetBird client v0.45 or higher.
|
||||
Changes will only take effect after restarting the clients.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<HelpText>
|
||||
Lazy connections are an experimental feature. Functionality and
|
||||
behavior may evolve. Instead of maintaining always-on connections,
|
||||
NetBird activates them on-demand based on activity or signaling.{" "}
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/lazy-connection"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</HelpText>
|
||||
<FancyToggleSwitch
|
||||
className={"mt-2"}
|
||||
value={lazyConnection}
|
||||
onChange={toggleLazyConnection}
|
||||
label={
|
||||
<>
|
||||
<ClockFadingIcon size={15} />
|
||||
Enable Lazy Connections
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Allow to establish connections between peers only when
|
||||
required. This requires NetBird client v0.45 or higher.
|
||||
Changes will only take effect after restarting the clients.
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,97 +14,97 @@ import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
const config = loadConfig();
|
||||
|
||||
export default function DangerZoneTab({ account }: Props) {
|
||||
const t = useTranslations("settings");
|
||||
const { confirm } = useDialog();
|
||||
const deleteRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
const { logout } = useLoggedInUser();
|
||||
const t = useTranslations("settings");
|
||||
const { confirm } = useDialog();
|
||||
const deleteRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
const { logout } = useLoggedInUser();
|
||||
|
||||
const deleteAccount = async () => {
|
||||
const deletePromise = new Promise<void>((resolve, reject) => {
|
||||
return deleteRequest
|
||||
.del()
|
||||
.then(() => {
|
||||
// Clear browser storage only after confirmed account deletion
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// Optionally, clear cookies if needed
|
||||
// document.cookie = ... (set cookies to expire)
|
||||
}
|
||||
logout().then();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
const deleteAccount = async () => {
|
||||
const deletePromise = new Promise<void>((resolve, reject) => {
|
||||
return deleteRequest
|
||||
.del()
|
||||
.then(() => {
|
||||
// Clear browser storage only after confirmed account deletion
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// Optionally, clear cookies if needed
|
||||
// document.cookie = ... (set cookies to expire)
|
||||
}
|
||||
logout().then();
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
|
||||
notify({
|
||||
title: "Delete NetBird account",
|
||||
description: "NetBird account was successfully deleted.",
|
||||
promise: deletePromise,
|
||||
loadingMessage: "Deleting the account...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Delete NetBird account",
|
||||
description: "NetBird account was successfully deleted.",
|
||||
promise: deletePromise,
|
||||
loadingMessage: "Deleting the account...",
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
const choice = await confirm({
|
||||
title: "Delete NetBird account",
|
||||
description:
|
||||
"Are you sure you want to delete your NetBird account? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
if (!choice) return;
|
||||
deleteAccount().then();
|
||||
};
|
||||
const handleConfirm = async () => {
|
||||
const choice = await confirm({
|
||||
title: "Delete NetBird account",
|
||||
description:
|
||||
"Are you sure you want to delete your NetBird account? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
if (!choice) return;
|
||||
deleteAccount().then();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"danger-zone"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("dangerZone")}
|
||||
icon={<AlertOctagonIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{t("dangerZone")}</h1>
|
||||
<div className={"gap-6 mt-6 max-w-lg"}>
|
||||
<Card
|
||||
className={
|
||||
"w-full flex flex-col gap-2 border-red-600 bg-red-950/50"
|
||||
}
|
||||
>
|
||||
<div className={"px-8 py-6"}>
|
||||
<p className={"text-xl font-medium mb-2 !text-red-50"}>
|
||||
Delete NetBird account
|
||||
</p>
|
||||
<p className={"!text-red-50/80"}>
|
||||
Before proceeding to delete your Netbird account, please be
|
||||
aware that this action is irreversible. Once your account is
|
||||
deleted, you will permanently lose access to all associated
|
||||
data, including your peers, users, groups, policies, and routes.
|
||||
</p>
|
||||
<div className={"mt-6"}>
|
||||
<Button variant={"danger"} onClick={handleConfirm} size={"xs"}>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
return (
|
||||
<Tabs.Content value={"danger-zone"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("dangerZone")}
|
||||
icon={<AlertOctagonIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{t("dangerZone")}</h1>
|
||||
<div className={"gap-6 mt-6 max-w-lg"}>
|
||||
<Card
|
||||
className={
|
||||
"w-full flex flex-col gap-2 border-red-600 bg-red-950/50"
|
||||
}
|
||||
>
|
||||
<div className={"px-8 py-6"}>
|
||||
<p className={"text-xl font-medium mb-2 !text-red-50"}>
|
||||
Delete NetBird account
|
||||
</p>
|
||||
<p className={"!text-red-50/80"}>
|
||||
Before proceeding to delete your Netbird account, please be
|
||||
aware that this action is irreversible. Once your account is
|
||||
deleted, you will permanently lose access to all associated
|
||||
data, including your peers, users, groups, policies, and routes.
|
||||
</p>
|
||||
<div className={"mt-6"}>
|
||||
<Button variant={"danger"} onClick={handleConfirm} size={"xs"}>
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import { isLocalDev, isNetBirdHosted } from "@utils/netbird";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
AlertCircle,
|
||||
Braces,
|
||||
FolderGit2Icon,
|
||||
FolderInput,
|
||||
FolderSync,
|
||||
ShieldCheck,
|
||||
X,
|
||||
AlertCircle,
|
||||
Braces,
|
||||
FolderGit2Icon,
|
||||
FolderInput,
|
||||
FolderSync,
|
||||
ShieldCheck,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -34,299 +34,299 @@ import { InlineButtonLink } from "@components/InlineLink";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function GroupsSettings({ account }: Props) {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const router = useRouter();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const router = useRouter();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
/**
|
||||
* Group Propagation
|
||||
*/
|
||||
const [groupsPropagation, setGroupsPropagation] = useState<boolean>(
|
||||
account.settings.groups_propagation_enabled,
|
||||
);
|
||||
/**
|
||||
* Group Propagation
|
||||
*/
|
||||
const [groupsPropagation, setGroupsPropagation] = useState<boolean>(
|
||||
account.settings.groups_propagation_enabled,
|
||||
);
|
||||
|
||||
/**
|
||||
* JWT Group Sync
|
||||
*/
|
||||
const [jwtGroupSync, setJwtGroupSync] = useState<boolean>(
|
||||
account.settings.jwt_groups_enabled,
|
||||
);
|
||||
const [jwtGroupsClaimName, setJwtGroupsClaimName] = useState(
|
||||
account.settings.jwt_groups_claim_name,
|
||||
);
|
||||
const [jwtAllowGroups, setJwtAllowGroups] = useState<string[]>(
|
||||
account.settings.jwt_allow_groups,
|
||||
);
|
||||
const [jwtAllowGroupsWarning, setJwtAllowGroupsWarning] = useState(false);
|
||||
/**
|
||||
* JWT Group Sync
|
||||
*/
|
||||
const [jwtGroupSync, setJwtGroupSync] = useState<boolean>(
|
||||
account.settings.jwt_groups_enabled,
|
||||
);
|
||||
const [jwtGroupsClaimName, setJwtGroupsClaimName] = useState(
|
||||
account.settings.jwt_groups_claim_name,
|
||||
);
|
||||
const [jwtAllowGroups, setJwtAllowGroups] = useState<string[]>(
|
||||
account.settings.jwt_allow_groups,
|
||||
);
|
||||
const [jwtAllowGroupsWarning, setJwtAllowGroupsWarning] = useState(false);
|
||||
|
||||
/**
|
||||
* Detect changes
|
||||
*/
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
groupsPropagation,
|
||||
jwtAllowGroups,
|
||||
jwtGroupsClaimName,
|
||||
jwtGroupSync,
|
||||
]);
|
||||
/**
|
||||
* Detect changes
|
||||
*/
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
groupsPropagation,
|
||||
jwtAllowGroups,
|
||||
jwtGroupsClaimName,
|
||||
jwtGroupSync,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Save Group Propagation
|
||||
*/
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
/**
|
||||
* Save Group Propagation
|
||||
*/
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const jwtGroupsEntered =
|
||||
jwtAllowGroups.filter((g) => !isEmpty(g)).length > 0;
|
||||
const showConfirm = jwtGroupSync && jwtGroupsEntered;
|
||||
const choice = showConfirm
|
||||
? await confirm({
|
||||
title: `JWT allow group - ${jwtAllowGroups[0]}`,
|
||||
description: `Only users part of the ${jwtAllowGroups[0]} group will be able to access NetBird. Are you sure you want to save the changes?`,
|
||||
confirmText: "Save",
|
||||
children: (
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center text-xs bg-netbird-950 px-4 justify-center py-3 rounded-md border border-netbird-500 text-netbird-200"
|
||||
}
|
||||
>
|
||||
<AlertCircle size={14} />
|
||||
To prevent losing access, ensure you are part of this group.
|
||||
</div>
|
||||
),
|
||||
cancelText: "Cancel",
|
||||
type: "default",
|
||||
})
|
||||
: true;
|
||||
const saveChanges = async () => {
|
||||
const jwtGroupsEntered =
|
||||
jwtAllowGroups.filter((g) => !isEmpty(g)).length > 0;
|
||||
const showConfirm = jwtGroupSync && jwtGroupsEntered;
|
||||
const choice = showConfirm
|
||||
? await confirm({
|
||||
title: `JWT allow group - ${jwtAllowGroups[0]}`,
|
||||
description: `Only users part of the ${jwtAllowGroups[0]} group will be able to access NetBird. Are you sure you want to save the changes?`,
|
||||
confirmText: "Save",
|
||||
children: (
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center text-xs bg-netbird-950 px-4 justify-center py-3 rounded-md border border-netbird-500 text-netbird-200"
|
||||
}
|
||||
>
|
||||
<AlertCircle size={14} />
|
||||
To prevent losing access, ensure you are part of this group.
|
||||
</div>
|
||||
),
|
||||
cancelText: "Cancel",
|
||||
type: "default",
|
||||
})
|
||||
: true;
|
||||
|
||||
if (!choice) return;
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: "Group Settings",
|
||||
description: "Group settings were updated successfully.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
groups_propagation_enabled: groupsPropagation,
|
||||
jwt_groups_enabled: jwtGroupSync,
|
||||
jwt_groups_claim_name: isEmpty(jwtGroupsClaimName)
|
||||
? undefined
|
||||
: jwtGroupsClaimName,
|
||||
jwt_allow_groups: jwtGroupsEntered ? jwtAllowGroups : undefined,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
groupsPropagation,
|
||||
jwtAllowGroups,
|
||||
jwtGroupsClaimName,
|
||||
jwtGroupSync,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating group settings...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Group Settings",
|
||||
description: "Group settings were updated successfully.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
groups_propagation_enabled: groupsPropagation,
|
||||
jwt_groups_enabled: jwtGroupSync,
|
||||
jwt_groups_claim_name: isEmpty(jwtGroupsClaimName)
|
||||
? undefined
|
||||
: jwtGroupsClaimName,
|
||||
jwt_allow_groups: jwtGroupsEntered ? jwtAllowGroups : undefined,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
groupsPropagation,
|
||||
jwtAllowGroups,
|
||||
jwtGroupsClaimName,
|
||||
jwtGroupSync,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating group settings...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"groups"} className={"w-full"}>
|
||||
<div className={"p-default py-6 max-w-xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("groupsTab")}
|
||||
icon={<FolderGit2Icon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("groupsTab")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"groups"} className={"w-full"}>
|
||||
<div className={"p-default py-6 max-w-xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("groupsTab")}
|
||||
icon={<FolderGit2Icon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("groupsTab")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 mt-8 mb-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={groupsPropagation}
|
||||
onChange={setGroupsPropagation}
|
||||
label={
|
||||
<>
|
||||
<FolderInput size={15} />
|
||||
Enable user group propagation
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow group propagation from user's auto-groups to peers, sharing membership information."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
{(!isNetBirdHosted() || isLocalDev()) && (
|
||||
<FancyToggleSwitch
|
||||
value={jwtGroupSync}
|
||||
onChange={setJwtGroupSync}
|
||||
label={
|
||||
<>
|
||||
<FolderSync size={15} />
|
||||
Enable JWT group sync
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Extract & sync groups from JWT claims with user's auto-groups, auto-creating groups from tokens."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex flex-col gap-6 mt-8 mb-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={groupsPropagation}
|
||||
onChange={setGroupsPropagation}
|
||||
label={
|
||||
<>
|
||||
<FolderInput size={15} />
|
||||
Enable user group propagation
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Allow group propagation from user's auto-groups to peers, sharing membership information."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
{(!isNetBirdHosted() || isLocalDev()) && (
|
||||
<FancyToggleSwitch
|
||||
value={jwtGroupSync}
|
||||
onChange={setJwtGroupSync}
|
||||
label={
|
||||
<>
|
||||
<FolderSync size={15} />
|
||||
Enable JWT group sync
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Extract & sync groups from JWT claims with user's auto-groups, auto-creating groups from tokens."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(!isNetBirdHosted() || isLocalDev()) && (
|
||||
<AnimatePresence>
|
||||
{jwtGroupSync && (
|
||||
<div className={"overflow-hidden -top-4 relative z-0"}>
|
||||
<motion.div
|
||||
className={""}
|
||||
initial={{ opacity: 0, height: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, height: "auto", scale: 1 }}
|
||||
exit={{ opacity: 0, height: 0, scale: 0.98 }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
!jwtGroupSync && "opacity-50 pointer-events-none",
|
||||
"flex flex-col gap-6 bg-nb-gray-940 px-6 pt-5 pb-6 border border-nb-gray-930 rounded-b-md relative mx-3",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Label>JWT claim</Label>
|
||||
<HelpText>
|
||||
Specify the JWT claim for extracting group names, e.g.,
|
||||
roles or groups, to add to account groups (this claim
|
||||
should contain a list of group names).
|
||||
</HelpText>
|
||||
<Input
|
||||
customPrefix={
|
||||
<Braces size={16} className={"text-nb-gray-300"} />
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.code === "Space") event.preventDefault();
|
||||
}}
|
||||
placeholder={"e.g., roles"}
|
||||
value={jwtGroupsClaimName}
|
||||
onChange={(e) => {
|
||||
setJwtGroupsClaimName(
|
||||
e.target.value.replace(/ /g, ""),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>JWT allow groups</Label>
|
||||
<HelpText>
|
||||
Limit access to NetBird for the specified group names,
|
||||
e.g., NetBird users. To use the groups, you need to
|
||||
configure them first in your IdP.
|
||||
</HelpText>
|
||||
<div>
|
||||
{jwtAllowGroups.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{jwtAllowGroups.map((group, index) => (
|
||||
<Badge
|
||||
key={group}
|
||||
variant={"gray-ghost"}
|
||||
className={cn(
|
||||
"transition-all group whitespace-nowrap cursor-pointer",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const newGroups = jwtAllowGroups.filter(
|
||||
(_, i) => i !== index,
|
||||
);
|
||||
setJwtAllowGroups(newGroups);
|
||||
setJwtAllowGroupsWarning(
|
||||
newGroups.length > 0,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
<X
|
||||
size={12}
|
||||
className={
|
||||
"cursor-pointer group-hover:text-nb-gray-100 transition-all shrink-0"
|
||||
}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
customPrefix={
|
||||
<ShieldCheck
|
||||
size={16}
|
||||
className={"text-nb-gray-300"}
|
||||
/>
|
||||
}
|
||||
placeholder={"Add a group and press Enter"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
if (input.value.trim()) {
|
||||
setJwtAllowGroups([
|
||||
...jwtAllowGroups,
|
||||
input.value.trim(),
|
||||
]);
|
||||
setJwtAllowGroupsWarning(true);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{jwtAllowGroupsWarning && (
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center text-xs bg-netbird-950 px-4 justify-center py-3 rounded-md border border-netbird-500 text-netbird-200"
|
||||
}
|
||||
>
|
||||
<AlertCircle size={14} />
|
||||
To prevent losing access, ensure you are part of this
|
||||
group.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
{(!isNetBirdHosted() || isLocalDev()) && (
|
||||
<AnimatePresence>
|
||||
{jwtGroupSync && (
|
||||
<div className={"overflow-hidden -top-4 relative z-0"}>
|
||||
<motion.div
|
||||
className={""}
|
||||
initial={{ opacity: 0, height: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, height: "auto", scale: 1 }}
|
||||
exit={{ opacity: 0, height: 0, scale: 0.98 }}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
!jwtGroupSync && "opacity-50 pointer-events-none",
|
||||
"flex flex-col gap-6 bg-nb-gray-940 px-6 pt-5 pb-6 border border-nb-gray-930 rounded-b-md relative mx-3",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Label>JWT claim</Label>
|
||||
<HelpText>
|
||||
Specify the JWT claim for extracting group names, e.g.,
|
||||
roles or groups, to add to account groups (this claim
|
||||
should contain a list of group names).
|
||||
</HelpText>
|
||||
<Input
|
||||
customPrefix={
|
||||
<Braces size={16} className={"text-nb-gray-300"} />
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (event.code === "Space") event.preventDefault();
|
||||
}}
|
||||
placeholder={"e.g., roles"}
|
||||
value={jwtGroupsClaimName}
|
||||
onChange={(e) => {
|
||||
setJwtGroupsClaimName(
|
||||
e.target.value.replace(/ /g, ""),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>JWT allow groups</Label>
|
||||
<HelpText>
|
||||
Limit access to NetBird for the specified group names,
|
||||
e.g., NetBird users. To use the groups, you need to
|
||||
configure them first in your IdP.
|
||||
</HelpText>
|
||||
<div>
|
||||
{jwtAllowGroups.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{jwtAllowGroups.map((group, index) => (
|
||||
<Badge
|
||||
key={group}
|
||||
variant={"gray-ghost"}
|
||||
className={cn(
|
||||
"transition-all group whitespace-nowrap cursor-pointer",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const newGroups = jwtAllowGroups.filter(
|
||||
(_, i) => i !== index,
|
||||
);
|
||||
setJwtAllowGroups(newGroups);
|
||||
setJwtAllowGroupsWarning(
|
||||
newGroups.length > 0,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
<X
|
||||
size={12}
|
||||
className={
|
||||
"cursor-pointer group-hover:text-nb-gray-100 transition-all shrink-0"
|
||||
}
|
||||
/>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Input
|
||||
customPrefix={
|
||||
<ShieldCheck
|
||||
size={16}
|
||||
className={"text-nb-gray-300"}
|
||||
/>
|
||||
}
|
||||
placeholder={"Add a group and press Enter"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const input = e.currentTarget;
|
||||
if (input.value.trim()) {
|
||||
setJwtAllowGroups([
|
||||
...jwtAllowGroups,
|
||||
input.value.trim(),
|
||||
]);
|
||||
setJwtAllowGroupsWarning(true);
|
||||
input.value = "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{jwtAllowGroupsWarning && (
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 items-center text-xs bg-netbird-950 px-4 justify-center py-3 rounded-md border border-netbird-500 text-netbird-200"
|
||||
}
|
||||
>
|
||||
<AlertCircle size={14} />
|
||||
To prevent losing access, ensure you are part of this
|
||||
group.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
|
||||
<Callout variant={"info"} className={"mt-6"}>
|
||||
Looking to view and manage your groups? You can find group management
|
||||
under{" "}
|
||||
<InlineButtonLink
|
||||
onClick={() => router.push("/groups")}
|
||||
variant={"dashed"}
|
||||
>
|
||||
{`Access Control › Groups`}
|
||||
</InlineButtonLink>
|
||||
</Callout>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<Callout variant={"info"} className={"mt-6"}>
|
||||
Looking to view and manage your groups? You can find group management
|
||||
under{" "}
|
||||
<InlineButtonLink
|
||||
onClick={() => router.push("/groups")}
|
||||
variant={"dashed"}
|
||||
>
|
||||
{`Access Control › Groups`}
|
||||
</InlineButtonLink>
|
||||
</Callout>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import { notify } from "@components/Notification";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -80,6 +81,7 @@ export default function IdentityProviderModal({
|
||||
onClose,
|
||||
provider,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const { mutate } = useSWRConfig();
|
||||
const { permission } = usePermissions();
|
||||
const isEditing = !!provider;
|
||||
@@ -277,7 +279,7 @@ export default function IdentityProviderModal({
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -12,12 +12,12 @@ import * as Tabs from "@radix-ui/react-tabs";
|
||||
import useFetchApi, { useApiCall } from "@utils/api";
|
||||
import { ColumnDef, SortingState } from "@tanstack/react-table";
|
||||
import {
|
||||
FingerprintIcon,
|
||||
KeyRound,
|
||||
MoreVertical,
|
||||
PencilIcon,
|
||||
PlusCircle,
|
||||
Trash2,
|
||||
FingerprintIcon,
|
||||
KeyRound,
|
||||
MoreVertical,
|
||||
PencilIcon,
|
||||
PlusCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -27,264 +27,264 @@ import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import {
|
||||
getSSOIdentityProviderLabelByType,
|
||||
SSOIdentityProvider,
|
||||
SSOIdentityProviderType,
|
||||
getSSOIdentityProviderLabelByType,
|
||||
SSOIdentityProvider,
|
||||
SSOIdentityProviderType,
|
||||
} from "@/interfaces/IdentityProvider";
|
||||
import IdentityProviderModal from "@/modules/settings/IdentityProviderModal";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/DropdownMenu";
|
||||
import { idpIcon } from "@/assets/icons/IdentityProviderIcons";
|
||||
|
||||
export const idpTypeLabels: Record<SSOIdentityProviderType, string> = {
|
||||
oidc: "OIDC",
|
||||
zitadel: "Zitadel",
|
||||
entra: "Microsoft Entra",
|
||||
google: "Google",
|
||||
okta: "Okta",
|
||||
pocketid: "PocketID",
|
||||
microsoft: "Microsoft",
|
||||
authentik: "Authentik",
|
||||
keycloak: "Keycloak",
|
||||
adfs: "Microsoft AD FS",
|
||||
oidc: "OIDC",
|
||||
zitadel: "Zitadel",
|
||||
entra: "Microsoft Entra",
|
||||
google: "Google",
|
||||
okta: "Okta",
|
||||
pocketid: "PocketID",
|
||||
microsoft: "Microsoft",
|
||||
authentik: "Authentik",
|
||||
keycloak: "Keycloak",
|
||||
adfs: "Microsoft AD FS",
|
||||
};
|
||||
|
||||
type ActionCellProps = {
|
||||
provider: SSOIdentityProvider;
|
||||
onEdit: (provider: SSOIdentityProvider) => void;
|
||||
provider: SSOIdentityProvider;
|
||||
onEdit: (provider: SSOIdentityProvider) => void;
|
||||
};
|
||||
|
||||
function ActionCell({ provider, onEdit }: ActionCellProps) {
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
const deleteRequest = useApiCall<SSOIdentityProvider>(
|
||||
"/identity-providers/" + provider.id,
|
||||
);
|
||||
const { permission } = usePermissions();
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
const deleteRequest = useApiCall<SSOIdentityProvider>(
|
||||
"/identity-providers/" + provider.id,
|
||||
);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const handleDelete = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Delete '${provider.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this identity provider? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
const handleDelete = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Delete '${provider.name}'?`,
|
||||
description:
|
||||
"Are you sure you want to delete this identity provider? This action cannot be undone.",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!choice) return;
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: "Delete Identity Provider",
|
||||
description: "Identity provider was deleted successfully.",
|
||||
promise: deleteRequest.del().then(() => {
|
||||
mutate("/identity-providers");
|
||||
}),
|
||||
loadingMessage: "Deleting identity provider...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Delete Identity Provider",
|
||||
description: "Identity provider was deleted successfully.",
|
||||
promise: deleteRequest.del().then(() => {
|
||||
mutate("/identity-providers");
|
||||
}),
|
||||
loadingMessage: "Deleting identity provider...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" className="p-2">
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onEdit(provider)}
|
||||
disabled={!permission.identity_providers.update}
|
||||
>
|
||||
<PencilIcon size={14} className="mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={handleDelete}
|
||||
disabled={!permission.identity_providers.delete}
|
||||
className="text-red-500 focus:text-red-500"
|
||||
>
|
||||
<Trash2 size={14} className="mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" className="p-2">
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => onEdit(provider)}
|
||||
disabled={!permission.identity_providers.update}
|
||||
>
|
||||
<PencilIcon size={14} className="mr-2" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={handleDelete}
|
||||
disabled={!permission.identity_providers.delete}
|
||||
className="text-red-500 focus:text-red-500"
|
||||
>
|
||||
<Trash2 size={14} className="mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IdentityProvidersTab() {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: providers, isLoading } = useFetchApi<SSOIdentityProvider[]>(
|
||||
"/identity-providers",
|
||||
);
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: providers, isLoading } = useFetchApi<SSOIdentityProvider[]>(
|
||||
"/identity-providers",
|
||||
);
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editProvider, setEditProvider] = useState<SSOIdentityProvider | null>(
|
||||
null,
|
||||
);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editProvider, setEditProvider] = useState<SSOIdentityProvider | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [sorting, setSorting] = useLocalStorage<SortingState>(
|
||||
"netbird-table-sort-identity-providers",
|
||||
[
|
||||
{
|
||||
id: "name",
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
const [sorting, setSorting] = useLocalStorage<SortingState>(
|
||||
"netbird-table-sort-identity-providers",
|
||||
[
|
||||
{
|
||||
id: "name",
|
||||
desc: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const handleEdit = (provider: SSOIdentityProvider) => {
|
||||
setEditProvider(provider);
|
||||
setModalOpen(true);
|
||||
};
|
||||
const handleEdit = (provider: SSOIdentityProvider) => {
|
||||
setEditProvider(provider);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditProvider(null);
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setEditProvider(null);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<SSOIdentityProvider>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Name</DataTableHeader>
|
||||
),
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{idpIcon(row.original.type) || (
|
||||
<KeyRound size={16} className="text-nb-gray-400" />
|
||||
)}
|
||||
<span className="font-medium">{row.original.name}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Type</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-nb-gray-400">
|
||||
{getSSOIdentityProviderLabelByType(row.original.type)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<ActionCell provider={row.original} onEdit={handleEdit} />
|
||||
),
|
||||
},
|
||||
];
|
||||
const columns: ColumnDef<SSOIdentityProvider>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Name</DataTableHeader>
|
||||
),
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{idpIcon(row.original.type) || (
|
||||
<KeyRound size={16} className="text-nb-gray-400" />
|
||||
)}
|
||||
<span className="font-medium">{row.original.name}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Type</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-nb-gray-400">
|
||||
{getSSOIdentityProviderLabelByType(row.original.type)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
accessorKey: "id",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<ActionCell provider={row.original} onEdit={handleEdit} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"identity-providers"} className={"w-full"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=identity-providers"}
|
||||
label={t("identityProviders")}
|
||||
icon={<FingerprintIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("identityProviders")}</h1>
|
||||
<Paragraph>
|
||||
Configure identity providers for user authentication in your
|
||||
network.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"identity-providers"} className={"w-full"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=identity-providers"}
|
||||
label={t("identityProviders")}
|
||||
icon={<FingerprintIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("identityProviders")}</h1>
|
||||
<Paragraph>
|
||||
Configure identity providers for user authentication in your
|
||||
network.
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IdentityProviderModal
|
||||
open={modalOpen}
|
||||
key={modalOpen ? 1 : 0}
|
||||
onClose={handleCloseModal}
|
||||
provider={editProvider}
|
||||
/>
|
||||
<IdentityProviderModal
|
||||
open={modalOpen}
|
||||
key={modalOpen ? 1 : 0}
|
||||
onClose={handleCloseModal}
|
||||
provider={editProvider}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Identity Providers"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={columns}
|
||||
data={providers}
|
||||
onRowClick={(row) => handleEdit(row.original)}
|
||||
searchPlaceholder={"Search by name or type..."}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<FingerprintIcon size={20} />}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Add Identity Provider"}
|
||||
description={
|
||||
"Configure an identity provider to enable SSO authentication for your users."
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!permission.identity_providers.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Identity Provider
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{providers && providers.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!permission.identity_providers.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Identity Provider
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={!providers || providers.length === 0}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={!providers || providers.length === 0}
|
||||
onClick={() => mutate("/identity-providers")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<DataTable
|
||||
isLoading={isLoading}
|
||||
text={"Identity Providers"}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
columns={columns}
|
||||
data={providers}
|
||||
onRowClick={(row) => handleEdit(row.original)}
|
||||
searchPlaceholder={"Search by name or type..."}
|
||||
getStartedCard={
|
||||
<GetStartedTest
|
||||
icon={
|
||||
<SquareIcon
|
||||
icon={<FingerprintIcon size={20} />}
|
||||
color={"gray"}
|
||||
size={"large"}
|
||||
/>
|
||||
}
|
||||
title={"Add Identity Provider"}
|
||||
description={
|
||||
"Configure an identity provider to enable SSO authentication for your users."
|
||||
}
|
||||
button={
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!permission.identity_providers.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Identity Provider
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
rightSide={() => (
|
||||
<>
|
||||
{providers && providers.length > 0 && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={!permission.identity_providers.create}
|
||||
>
|
||||
<PlusCircle size={16} />
|
||||
Add Identity Provider
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<>
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={!providers || providers.length === 0}
|
||||
/>
|
||||
<DataTableRefreshButton
|
||||
isDisabled={!providers || providers.length === 0}
|
||||
onClick={() => mutate("/identity-providers")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,334 +25,334 @@ import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { SkeletonSettings } from "@components/skeletons/SkeletonSettings";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
const { isLoading: isGroupsLoading } = useGroups();
|
||||
|
||||
return isGroupsLoading ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<NetworkSettingsTabContent account={account} />
|
||||
);
|
||||
return isGroupsLoading ? (
|
||||
<SkeletonSettings />
|
||||
) : (
|
||||
<NetworkSettingsTabContent account={account} />
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkSettingsTabContent({ account }: Readonly<Props>) {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);
|
||||
|
||||
const [routingPeerDNSSetting, setRoutingPeerDNSSetting] = useState(
|
||||
account.settings.routing_peer_dns_resolution_enabled,
|
||||
);
|
||||
const [customDNSDomain, setCustomDNSDomain] = useState(
|
||||
account.settings.dns_domain || "",
|
||||
);
|
||||
const [networkRange, setNetworkRange] = useState(
|
||||
account.settings.network_range || "",
|
||||
);
|
||||
const [networkRangeV6, setNetworkRangeV6] = useState(
|
||||
account.settings.network_range_v6 || "",
|
||||
);
|
||||
const [ipv6EnabledGroups, setIpv6EnabledGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: account.settings?.ipv6_enabled_groups,
|
||||
});
|
||||
const ipv6GroupNames = useMemo(
|
||||
() => ipv6EnabledGroups.map((g) => g.name).sort(),
|
||||
[ipv6EnabledGroups],
|
||||
);
|
||||
const [routingPeerDNSSetting, setRoutingPeerDNSSetting] = useState(
|
||||
account.settings.routing_peer_dns_resolution_enabled,
|
||||
);
|
||||
const [customDNSDomain, setCustomDNSDomain] = useState(
|
||||
account.settings.dns_domain || "",
|
||||
);
|
||||
const [networkRange, setNetworkRange] = useState(
|
||||
account.settings.network_range || "",
|
||||
);
|
||||
const [networkRangeV6, setNetworkRangeV6] = useState(
|
||||
account.settings.network_range_v6 || "",
|
||||
);
|
||||
const [ipv6EnabledGroups, setIpv6EnabledGroups, { save: saveGroups }] =
|
||||
useGroupHelper({
|
||||
initial: account.settings?.ipv6_enabled_groups,
|
||||
});
|
||||
const ipv6GroupNames = useMemo(
|
||||
() => ipv6EnabledGroups.map((g) => g.name).sort(),
|
||||
[ipv6EnabledGroups],
|
||||
);
|
||||
|
||||
const toggleNetworkDNSSetting = async (toggle: boolean) => {
|
||||
notify({
|
||||
title: "DNS Wildcard Routing",
|
||||
description: `DNS Wildcard Routing successfully ${
|
||||
toggle ? "enabled" : "disabled"
|
||||
}.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
routing_peer_dns_resolution_enabled: toggle,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setRoutingPeerDNSSetting(toggle);
|
||||
mutate("/accounts");
|
||||
}),
|
||||
loadingMessage: "Updating DNS wildcard setting...",
|
||||
});
|
||||
};
|
||||
const toggleNetworkDNSSetting = async (toggle: boolean) => {
|
||||
notify({
|
||||
title: "DNS Wildcard Routing",
|
||||
description: `DNS Wildcard Routing successfully ${
|
||||
toggle ? "enabled" : "disabled"
|
||||
}.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
routing_peer_dns_resolution_enabled: toggle,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
setRoutingPeerDNSSetting(toggle);
|
||||
mutate("/accounts");
|
||||
}),
|
||||
loadingMessage: "Updating DNS wildcard setting...",
|
||||
});
|
||||
};
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
customDNSDomain,
|
||||
networkRange,
|
||||
networkRangeV6,
|
||||
ipv6GroupNames,
|
||||
]);
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
customDNSDomain,
|
||||
networkRange,
|
||||
networkRangeV6,
|
||||
ipv6GroupNames,
|
||||
]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const groups = await saveGroups();
|
||||
const ipv6EnabledGroupIds = groups
|
||||
.map((group) => group.id)
|
||||
.filter(Boolean) as string[];
|
||||
const saveChanges = async () => {
|
||||
const groups = await saveGroups();
|
||||
const ipv6EnabledGroupIds = groups
|
||||
.map((group) => group.id)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const updatedSettings = {
|
||||
...account.settings,
|
||||
ipv6_enabled_groups: ipv6EnabledGroupIds,
|
||||
};
|
||||
const updatedSettings = {
|
||||
...account.settings,
|
||||
ipv6_enabled_groups: ipv6EnabledGroupIds,
|
||||
};
|
||||
|
||||
if (customDNSDomain !== "" || account.settings.dns_domain) {
|
||||
updatedSettings.dns_domain = customDNSDomain;
|
||||
}
|
||||
if (customDNSDomain !== "" || account.settings.dns_domain) {
|
||||
updatedSettings.dns_domain = customDNSDomain;
|
||||
}
|
||||
|
||||
// Only send network ranges when the user actually changed them, to avoid
|
||||
// triggering a reallocation when the server hasn't stored an explicit override.
|
||||
if (networkRange !== (account.settings.network_range || "")) {
|
||||
updatedSettings.network_range = networkRange;
|
||||
} else {
|
||||
delete updatedSettings.network_range;
|
||||
}
|
||||
// Only send network ranges when the user actually changed them, to avoid
|
||||
// triggering a reallocation when the server hasn't stored an explicit override.
|
||||
if (networkRange !== (account.settings.network_range || "")) {
|
||||
updatedSettings.network_range = networkRange;
|
||||
} else {
|
||||
delete updatedSettings.network_range;
|
||||
}
|
||||
|
||||
if (networkRangeV6 !== (account.settings.network_range_v6 || "")) {
|
||||
updatedSettings.network_range_v6 = networkRangeV6;
|
||||
} else {
|
||||
delete updatedSettings.network_range_v6;
|
||||
}
|
||||
if (networkRangeV6 !== (account.settings.network_range_v6 || "")) {
|
||||
updatedSettings.network_range_v6 = networkRangeV6;
|
||||
} else {
|
||||
delete updatedSettings.network_range_v6;
|
||||
}
|
||||
|
||||
notify({
|
||||
title: "Network Settings",
|
||||
description: `Network settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: updatedSettings,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
customDNSDomain,
|
||||
networkRange,
|
||||
networkRangeV6,
|
||||
ipv6GroupNames,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating network settings...",
|
||||
});
|
||||
};
|
||||
notify({
|
||||
title: "Network Settings",
|
||||
description: `Network settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: updatedSettings,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([
|
||||
customDNSDomain,
|
||||
networkRange,
|
||||
networkRangeV6,
|
||||
ipv6GroupNames,
|
||||
]);
|
||||
}),
|
||||
loadingMessage: "Updating network settings...",
|
||||
});
|
||||
};
|
||||
|
||||
const domainError = useMemo(() => {
|
||||
if (customDNSDomain == "") return "";
|
||||
const valid = validator.isValidDomain(customDNSDomain, {
|
||||
allowWildcard: false,
|
||||
allowOnlyTld: false,
|
||||
});
|
||||
if (!valid) {
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
}, [customDNSDomain]);
|
||||
const domainError = useMemo(() => {
|
||||
if (customDNSDomain == "") return "";
|
||||
const valid = validator.isValidDomain(customDNSDomain, {
|
||||
allowWildcard: false,
|
||||
allowOnlyTld: false,
|
||||
});
|
||||
if (!valid) {
|
||||
return "Please enter a valid domain, e.g. example.com or intra.example.com";
|
||||
}
|
||||
}, [customDNSDomain]);
|
||||
|
||||
const networkRangeError = useMemo(() => {
|
||||
if (networkRange == "") {
|
||||
if (account.settings.network_range) {
|
||||
return "Network range cannot be empty";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
const networkRangeError = useMemo(() => {
|
||||
if (networkRange == "") {
|
||||
if (account.settings.network_range) {
|
||||
return "Network range cannot be empty";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const validCIDR = cidr.isValidCIDR(networkRange);
|
||||
if (!validCIDR) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
} catch (error) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
}, [networkRange, account.settings.network_range]);
|
||||
try {
|
||||
const validCIDR = cidr.isValidCIDR(networkRange);
|
||||
if (!validCIDR) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
} catch (error) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
}, [networkRange, account.settings.network_range]);
|
||||
|
||||
const networkRangeV6Error = useMemo(() => {
|
||||
if (networkRangeV6 == "") return "";
|
||||
if (!networkRangeV6.includes(":") || !cidr.isValidCIDR(networkRangeV6)) {
|
||||
return "Please enter a valid IPv6 CIDR range, e.g. fd00:1234::/64";
|
||||
}
|
||||
const prefixLen = parseInt(networkRangeV6.split("/")[1], 10);
|
||||
if (prefixLen < 48 || prefixLen > 112) {
|
||||
return "Prefix length must be between /48 and /112";
|
||||
}
|
||||
}, [networkRangeV6]);
|
||||
const networkRangeV6Error = useMemo(() => {
|
||||
if (networkRangeV6 == "") return "";
|
||||
if (!networkRangeV6.includes(":") || !cidr.isValidCIDR(networkRangeV6)) {
|
||||
return "Please enter a valid IPv6 CIDR range, e.g. fd00:1234::/64";
|
||||
}
|
||||
const prefixLen = parseInt(networkRangeV6.split("/")[1], 10);
|
||||
if (prefixLen < 48 || prefixLen > 112) {
|
||||
return "Prefix length must be between /48 and /112";
|
||||
}
|
||||
}, [networkRangeV6]);
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"networks"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=networks"}
|
||||
label={t("networksTab")}
|
||||
icon={<NetworkIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("networksTab")}</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
!!domainError ||
|
||||
!!networkRangeError ||
|
||||
!!networkRangeV6Error
|
||||
}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"networks"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=networks"}
|
||||
label={t("networksTab")}
|
||||
icon={<NetworkIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<div>
|
||||
<h1>{t("networksTab")}</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
!!domainError ||
|
||||
!!networkRangeError ||
|
||||
!!networkRangeV6Error
|
||||
}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 w-full mt-8"}>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>DNS Domain</Label>
|
||||
<HelpText>
|
||||
Specify a custom peer DNS domain for your network. This should
|
||||
not point to a domain that is already in use elsewhere, to
|
||||
avoid overriding DNS results.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={
|
||||
isNetBirdHosted() ? "netbird.cloud" : "netbird.selfhosted"
|
||||
}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={domainError}
|
||||
value={customDNSDomain}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setCustomDNSDomain(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex flex-col gap-6 w-full mt-8"}>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>DNS Domain</Label>
|
||||
<HelpText>
|
||||
Specify a custom peer DNS domain for your network. This should
|
||||
not point to a domain that is already in use elsewhere, to
|
||||
avoid overriding DNS results.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={
|
||||
isNetBirdHosted() ? "netbird.cloud" : "netbird.selfhosted"
|
||||
}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={domainError}
|
||||
value={customDNSDomain}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setCustomDNSDomain(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>Network Range</Label>
|
||||
<HelpText>
|
||||
Specify a custom IPv4 range for your network in CIDR format.
|
||||
All peer IPs will be re-allocated when changed.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={"e.g. 100.64.0.0/16"}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={networkRangeError}
|
||||
value={networkRange}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setNetworkRange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>Network Range</Label>
|
||||
<HelpText>
|
||||
Specify a custom IPv4 range for your network in CIDR format.
|
||||
All peer IPs will be re-allocated when changed.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={"e.g. 100.64.0.0/16"}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={networkRangeError}
|
||||
value={networkRange}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setNetworkRange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>IPv6 Network Range</Label>
|
||||
<HelpText>
|
||||
Specify a custom IPv6 range for your network in CIDR format.
|
||||
All peer IPv6 addresses will be re-allocated when changed.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={"e.g. fd00:1234:5678::/64"}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={networkRangeV6Error}
|
||||
value={networkRangeV6}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setNetworkRangeV6(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>IPv6 Network Range</Label>
|
||||
<HelpText>
|
||||
Specify a custom IPv6 range for your network in CIDR format.
|
||||
All peer IPv6 addresses will be re-allocated when changed.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={"e.g. fd00:1234:5678::/64"}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={networkRangeV6Error}
|
||||
value={networkRangeV6}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setNetworkRangeV6(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>IPv6 Enabled Groups</Label>
|
||||
<HelpText>
|
||||
Peers in the selected groups will receive IPv6 overlay addresses
|
||||
(dual-stack). Remove all groups to disable IPv6. Changes apply on
|
||||
save and will restart affected clients.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
values={ipv6EnabledGroups}
|
||||
onChange={setIpv6EnabledGroups}
|
||||
placeholder="Select groups to enable IPv6..."
|
||||
showResourceCounter={false}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>IPv6 Enabled Groups</Label>
|
||||
<HelpText>
|
||||
Peers in the selected groups will receive IPv6 overlay addresses
|
||||
(dual-stack). Remove all groups to disable IPv6. Changes apply on
|
||||
save and will restart affected clients.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
values={ipv6EnabledGroups}
|
||||
onChange={setIpv6EnabledGroups}
|
||||
placeholder="Select groups to enable IPv6..."
|
||||
showResourceCounter={false}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"mt-4"} />
|
||||
<div className={"mt-4"} />
|
||||
|
||||
<FancyToggleSwitch
|
||||
value={routingPeerDNSSetting}
|
||||
onChange={toggleNetworkDNSSetting}
|
||||
label={
|
||||
<>
|
||||
<GlobeIcon size={15} />
|
||||
Enable DNS Wildcard Routing
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Allow routing using DNS wildcards. This requires NetBird client
|
||||
v0.35 or higher. Changes will only take effect after restarting
|
||||
the clients.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
|
||||
}
|
||||
target={"_blank"}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<FancyToggleSwitch
|
||||
value={routingPeerDNSSetting}
|
||||
onChange={toggleNetworkDNSSetting}
|
||||
label={
|
||||
<>
|
||||
<GlobeIcon size={15} />
|
||||
Enable DNS Wildcard Routing
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
<>
|
||||
Allow routing using DNS wildcards. This requires NetBird client
|
||||
v0.35 or higher. Changes will only take effect after restarting
|
||||
the clients.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
|
||||
}
|
||||
target={"_blank"}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</>
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,86 +14,86 @@ import { useHasChanges } from "@/hooks/useHasChanges";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function PermissionsTab({ account }: Props) {
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
const t = useTranslations("settings");
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
const { mutate } = useSWRConfig();
|
||||
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
|
||||
const [userViewBlocked, setUserViewBlocked] = useState<boolean>(
|
||||
account?.settings.regular_users_view_blocked ?? false,
|
||||
);
|
||||
const [userViewBlocked, setUserViewBlocked] = useState<boolean>(
|
||||
account?.settings.regular_users_view_blocked ?? false,
|
||||
);
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([userViewBlocked]);
|
||||
const { hasChanges, updateRef } = useHasChanges([userViewBlocked]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
notify({
|
||||
title: "Permission Settings",
|
||||
description: "Permissions were updated successfully.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
regular_users_view_blocked: userViewBlocked,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([userViewBlocked]);
|
||||
}),
|
||||
loadingMessage: "Updating permissions...",
|
||||
});
|
||||
};
|
||||
const saveChanges = async () => {
|
||||
notify({
|
||||
title: "Permission Settings",
|
||||
description: "Permissions were updated successfully.",
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
regular_users_view_blocked: userViewBlocked,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([userViewBlocked]);
|
||||
}),
|
||||
loadingMessage: "Updating permissions...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"permissions"} className={"w-full"}>
|
||||
<div className={"p-default py-6 max-w-xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=permissions"}
|
||||
label={t("permissions")}
|
||||
icon={<LockIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("permissions")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges || !permission.settings.update}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<Tabs.Content value={"permissions"} className={"w-full"}>
|
||||
<div className={"p-default py-6 max-w-xl"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=permissions"}
|
||||
label={t("permissions")}
|
||||
icon={<LockIcon size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className={"flex items-start justify-between"}>
|
||||
<h1>{t("permissions")}</h1>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges || !permission.settings.update}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-6 mt-8 mb-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={userViewBlocked}
|
||||
onChange={setUserViewBlocked}
|
||||
label={
|
||||
<>
|
||||
<GaugeIcon size={15} />
|
||||
Restrict dashboard for regular users
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Access to the dashboard will be limited and regular users will not be able to view any peers."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
<div className={"flex flex-col gap-6 mt-8 mb-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={userViewBlocked}
|
||||
onChange={setUserViewBlocked}
|
||||
label={
|
||||
<>
|
||||
<GaugeIcon size={15} />
|
||||
Restrict dashboard for regular users
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Access to the dashboard will be limited and regular users will not be able to view any peers."
|
||||
}
|
||||
disabled={!permission.settings.update}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,71 +15,71 @@ import { Group } from "@/interfaces/Group";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
|
||||
const SetupKeysTable = lazy(
|
||||
() => import("@/modules/setup-keys/SetupKeysTable"),
|
||||
() => import("@/modules/setup-keys/SetupKeysTable"),
|
||||
);
|
||||
|
||||
export default function SetupKeysTab() {
|
||||
const t = useTranslations("settings");
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { permission } = usePermissions();
|
||||
const { groups } = useGroups();
|
||||
const t = useTranslations("settings");
|
||||
const { data: setupKeys, isLoading } = useFetchApi<SetupKey[]>("/setup-keys");
|
||||
const { permission } = usePermissions();
|
||||
const { groups } = useGroups();
|
||||
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => groups.find((g) => g.id === group) || undefined)
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
const setupKeysWithGroups = useMemo(() => {
|
||||
if (!setupKeys) return [];
|
||||
return setupKeys.map((setupKey) => {
|
||||
if (!setupKey.auto_groups) return setupKey;
|
||||
if (!groups) return setupKey;
|
||||
return {
|
||||
...setupKey,
|
||||
groups: setupKey.auto_groups
|
||||
?.map((group) => groups.find((g) => g.id === group) || undefined)
|
||||
.filter((group) => group !== undefined) as Group[],
|
||||
};
|
||||
});
|
||||
}, [setupKeys, groups]);
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"setup-keys"} className={"w-full"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=setup-keys"}
|
||||
label={t("setupKeys")}
|
||||
icon={<KeyRound size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{t("setupKeys")}</h1>
|
||||
<Paragraph>
|
||||
Setup keys are pre-authentication keys that allow to register new
|
||||
machines in your network.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Setup Keys"}
|
||||
hasAccess={permission.setup_keys.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SetupKeysTable
|
||||
setupKeys={setupKeysWithGroups}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tabs.Content value={"setup-keys"} className={"w-full"}>
|
||||
<div className={"p-default py-6"}>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings"}
|
||||
label={t("title")}
|
||||
icon={<SettingsIcon size={13} />}
|
||||
/>
|
||||
<Breadcrumbs.Item
|
||||
href={"/settings?tab=setup-keys"}
|
||||
label={t("setupKeys")}
|
||||
icon={<KeyRound size={14} />}
|
||||
active
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1>{t("setupKeys")}</h1>
|
||||
<Paragraph>
|
||||
Setup keys are pre-authentication keys that allow to register new
|
||||
machines in your network.{" "}
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Learn more
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<RestrictedAccess
|
||||
page={"Setup Keys"}
|
||||
hasAccess={permission.setup_keys.read}
|
||||
>
|
||||
<Suspense fallback={<SkeletonTable />}>
|
||||
<SetupKeysTable
|
||||
setupKeys={setupKeysWithGroups}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</RestrictedAccess>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import { Group } from "@/interfaces/Group";
|
||||
import { SetupKey } from "@/interfaces/SetupKey";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode;
|
||||
@@ -57,6 +58,7 @@ export default function SetupKeyModal({
|
||||
showOnlyRoutingPeerOS,
|
||||
groups,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [successModal, setSuccessModal] = useState(false);
|
||||
const [setupKey, setSetupKey] = useState<SetupKey>();
|
||||
const [installModal, setInstallModal] = useState(false);
|
||||
@@ -170,6 +172,7 @@ export function SetupKeyModalContent({
|
||||
predefinedName = "",
|
||||
groups,
|
||||
}: Readonly<ModalProps>) {
|
||||
const t = useTranslations("common");
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -380,7 +383,7 @@ export function SetupKeyModalContent({
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -17,6 +17,7 @@ import HelpText from "@components/HelpText";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { KeyRound, LockIcon } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -27,6 +28,7 @@ export default function ChangePasswordModal({
|
||||
children,
|
||||
userId,
|
||||
}: Readonly<Props>) {
|
||||
const t = useTranslations("common");
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -49,6 +51,7 @@ export function ChangePasswordModalContent({
|
||||
userId,
|
||||
onSuccess,
|
||||
}: Readonly<ModalProps>) {
|
||||
const t = useTranslations("common");
|
||||
const passwordRequest = useApiCall<void>(`/users/${userId}/password`, true);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
@@ -177,7 +180,7 @@ export function ChangePasswordModalContent({
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -21,6 +21,7 @@ import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { Role, User } from "@/interfaces/User";
|
||||
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
@@ -47,6 +48,8 @@ export function ServiceUserModalContent({ onSuccess }: Readonly<ModalProps>) {
|
||||
const [name, setName] = useState("");
|
||||
const [role, setRole] = useState("user");
|
||||
|
||||
const t = useTranslations("common");
|
||||
|
||||
const create = async () => {
|
||||
notify({
|
||||
title: "Service user created",
|
||||
@@ -123,7 +126,7 @@ export function ServiceUserModalContent({ onSuccess }: Readonly<ModalProps>) {
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
<Button variant={"secondary"}>{t("cancel")}</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Suspense, useMemo } from "react";
|
||||
import { usePortalElement } from "@hooks/usePortalElement";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SkeletonTable, {
|
||||
SkeletonTableHeader,
|
||||
SkeletonTableHeader,
|
||||
} from "@components/skeletons/SkeletonTable";
|
||||
import { User } from "@/interfaces/User";
|
||||
import useFetchApi from "@utils/api";
|
||||
@@ -14,61 +14,62 @@ import PeerIcon from "@/assets/icons/PeerIcon";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const UserPeersSection = ({ user }: Props) => {
|
||||
const t = useTranslations("peers"); const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
const t = useTranslations("peers");
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
const { data: peers, isLoading: isPeersLoading } =
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
const { data: peers, isLoading: isPeersLoading } =
|
||||
useFetchApi<Peer[]>("/peers");
|
||||
|
||||
const userPeers = useMemo(() => {
|
||||
return (
|
||||
peers?.filter((peer) => {
|
||||
return peer?.user_id === user.id;
|
||||
}) || []
|
||||
);
|
||||
}, [user, peers]);
|
||||
const userPeers = useMemo(() => {
|
||||
return (
|
||||
peers?.filter((peer) => {
|
||||
return peer?.user_id === user.id;
|
||||
}) || []
|
||||
);
|
||||
}, [user, peers]);
|
||||
|
||||
return (
|
||||
<div className={"pb-10 px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<h2 ref={headingRef}>{t("title")}</h2>
|
||||
<Paragraph>{t("userPeersDescription")}</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className={"pb-10 px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center mb-5"}>
|
||||
<div>
|
||||
<h2 ref={headingRef}>{t("title")}</h2>
|
||||
<Paragraph>{t("userPeersDescription")}</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<SkeletonTableHeader className={"!p-0"} />
|
||||
<div className={"mt-8 w-full"}>
|
||||
<SkeletonTable withHeader={false} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MinimalPeersTable
|
||||
isLoading={isPeersLoading}
|
||||
peers={userPeers}
|
||||
headingTarget={portalTarget}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={"This user has no registered peers"}
|
||||
description={
|
||||
"Install NetBird and sign in as this user to register peers."
|
||||
}
|
||||
icon={<PeerIcon size={20} className={"fill-nb-gray-300"} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<Suspense
|
||||
fallback={
|
||||
<div>
|
||||
<SkeletonTableHeader className={"!p-0"} />
|
||||
<div className={"mt-8 w-full"}>
|
||||
<SkeletonTable withHeader={false} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MinimalPeersTable
|
||||
isLoading={isPeersLoading}
|
||||
peers={userPeers}
|
||||
headingTarget={portalTarget}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={"This user has no registered peers"}
|
||||
description={
|
||||
"Install NetBird and sign in as this user to register peers."
|
||||
}
|
||||
icon={<PeerIcon size={20} className={"fill-nb-gray-300"} />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user