Compare commits

..
Author SHA1 Message Date
braginini 92528d8377 Clear up password from the state 2025-12-29 09:17:30 -05:00
braginini 876f803033 Create user for self-hosted 2025-12-29 08:56:52 -05:00
braginini 43286352d1 Add user creation with password copy 2025-12-25 19:35:19 -05:00
5 changed files with 90 additions and 153 deletions

No files matched your search

-1
View File
@@ -22,7 +22,6 @@ export interface Account {
dns_domain: string;
network_range?: string;
lazy_connection_enabled: boolean;
auto_update_version: string;
};
onboarding?: AccountOnboarding;
}
+1
View File
@@ -13,6 +13,7 @@ export interface User {
pending_approval?: boolean;
last_login?: Date;
permissions: Permissions;
password?: string;
}
export enum Role {
+2 -137
View File
@@ -1,27 +1,16 @@
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import {
SelectDropdown,
SelectOption,
} from "@components/select/SelectDropdown";
import { useHasChanges } from "@hooks/useHasChanges";
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
import { validator } from "@utils/helpers";
import {
ClockFadingIcon,
ExternalLinkIcon,
FlaskConicalIcon,
MonitorSmartphoneIcon,
RefreshCcw,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import SettingsIcon from "@/assets/icons/SettingsIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
@@ -31,21 +20,6 @@ type Props = {
account: Account;
};
const latestOrCustomVersion = [
{
label: "Disabled",
value: "disabled",
},
{
label: "Latest Version",
value: "latest",
},
{
label: "Custom Version",
value: "custom",
},
] as SelectOption[];
export default function ClientSettingsTab({ account }: Readonly<Props>) {
const { permission } = usePermissions();
@@ -56,77 +30,6 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
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 [autoUpdateCustomVersion, setAutoUpdateCustomVersion] = useState(
isCustomVersion ? autoUpdateSetting : "",
);
const { hasChanges, updateRef } = useHasChanges([
autoUpdateMethod,
autoUpdateCustomVersion,
]);
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 canSaveCustomVersion =
autoUpdateCustomVersion !== "" &&
autoUpdateMethod === "custom" &&
versionError === "";
const isSaveButtonDisabled = useMemo(() => {
return (
!hasChanges ||
!permission.settings.update ||
(autoUpdateMethod === "custom" && !canSaveCustomVersion)
);
}, [
hasChanges,
permission.settings.update,
autoUpdateMethod,
canSaveCustomVersion,
]);
const saveChanges = async () => {
notify({
title: "Client Settings",
description: `Client settings successfully updated.`,
promise: saveRequest
.put({
id: account.id,
settings: {
...account.settings,
auto_update_version: autoUpdateCustomVersion || autoUpdateMethod,
},
})
.then(() => {
mutate("/accounts");
updateRef([autoUpdateMethod, autoUpdateCustomVersion]);
}),
loadingMessage: "Updating client settings...",
});
};
const toggleLazyConnection = async (toggle: boolean) => {
notify({
title: "Lazy Connections",
@@ -167,48 +70,10 @@ export default function ClientSettingsTab({ account }: Readonly<Props>) {
</Breadcrumbs>
<div className={"flex items-start justify-between"}>
<h1>Clients</h1>
<Button
variant={"primary"}
disabled={isSaveButtonDisabled}
onClick={saveChanges}
data-cy={"save-clients-settings"}
>
Save Changes
</Button>
</div>
<div className={"flex flex-col gap-6 w-full mt-8"}>
<div className={"flex flex-col relative"}>
<Label>
<RefreshCcw size={15} />
Automatic Updates
</Label>
<HelpText>
Select how NetBird clients handle automatic updates by choosing
the latest version, a custom version, or disabling updates
altogether.
</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>
</div>
<div className={"mt-3"}>
<div className={"mt-0"}>
<h2 className={"text-lg font-medium"}>
Experimental
<FlaskConicalIcon
+85 -12
View File
@@ -1,4 +1,5 @@
import Button from "@components/Button";
import Code from "@components/Code";
import HelpText from "@components/HelpText";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
@@ -14,10 +15,11 @@ import { PeerGroupSelector } from "@components/PeerGroupSelector";
import { IconMailForward } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn, validator } from "@utils/helpers";
import { MailIcon, User2 } from "lucide-react";
import { CopyIcon, MailIcon, User2 } from "lucide-react";
import Image from "next/image";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import useCopyToClipboard from "@/hooks/useCopyToClipboard";
import Avatar1 from "@/assets/avatars/009.jpg";
import Avatar2 from "@/assets/avatars/030.jpg";
import Avatar3 from "@/assets/avatars/063.jpg";
@@ -26,33 +28,104 @@ import { Group } from "@/interfaces/Group";
import { Role, User } from "@/interfaces/User";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
import {isNetBirdHosted} from "@utils/netbird";
type Props = {
children: React.ReactNode;
groups?: Group[];
};
const copyMessage = "Password was copied to your clipboard!";
export default function UserInviteModal({ children, groups }: Readonly<Props>) {
const [open, setOpen] = useState(false);
const [successModal, setSuccessModal] = useState(false);
const [createdUser, setCreatedUser] = useState<User>();
const { mutate } = useSWRConfig();
const [, copyToClipboard] = useCopyToClipboard(createdUser?.password);
const handleOnSuccess = () => {
setOpen(false);
const handleOnSuccess = (user: User) => {
if (user.password) {
setCreatedUser(user);
setSuccessModal(true);
} else {
setOpen(false);
}
setTimeout(() => {
mutate("/users?service_user=false");
}, 1000);
};
const handleCopyAndClose = () => {
copyToClipboard(copyMessage).then(() => {
setCreatedUser(undefined);
setSuccessModal(false);
setOpen(false);
});
};
return (
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
<ModalTrigger asChild={true}>{children}</ModalTrigger>
<UserInviteModalContent onSuccess={handleOnSuccess} groups={groups} />
</Modal>
<>
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
<ModalTrigger asChild={true}>{children}</ModalTrigger>
<UserInviteModalContent onSuccess={handleOnSuccess} groups={groups} />
</Modal>
<Modal
open={successModal}
onOpenChange={(open) => {
if (!open) {
setCreatedUser(undefined);
}
setSuccessModal(open);
setOpen(open);
}}
>
<ModalContent
onEscapeKeyDown={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
maxWidthClass={"max-w-md"}
className={"mt-20"}
showClose={false}
>
<div className={"pb-6 px-8"}>
<div className={"flex flex-col items-center justify-center gap-3"}>
<div>
<h2 className={"text-2xl text-center mb-2"}>
User created successfully!
</h2>
<Paragraph className={"mt-0 text-sm text-center"}>
This password will not be shown again, so be sure to copy it
and store in a secure location.
</Paragraph>
</div>
</div>
</div>
<div className={"px-8 pb-6"}>
<Code message={copyMessage}>
<Code.Line>{createdUser?.password || ""}</Code.Line>
</Code>
</div>
<ModalFooter className={"items-center"}>
<Button
variant={"primary"}
className={"w-full"}
onClick={handleCopyAndClose}
>
<CopyIcon size={14} />
Copy & Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}
type ModalProps = {
onSuccess: () => void;
onSuccess: (user: User) => void;
groups?: Group[];
};
@@ -85,9 +158,9 @@ export function UserInviteModalContent({
auto_groups: groupIds,
is_service_user: false,
})
.then(() => {
.then((user) => {
mutate("/users?service_user=false");
onSuccess && onSuccess();
onSuccess && onSuccess(user);
}),
loadingMessage: "Sending invite...",
});
@@ -121,10 +194,10 @@ export function UserInviteModalContent({
}
>
<h2 className={"text-lg my-0 leading-[1.5 text-center]"}>
Invite User
{isNetBirdHosted() ? "Invite User" : "Create User"}
</h2>
<Paragraph className={cn("text-sm text-center max-w-xs")}>
Invite a user to your network and set their permissions.
{isNetBirdHosted() ? "Invite a user to your network and set their permissions." : "Create a NetBird user account with email and password."}
</Paragraph>
</div>
+2 -3
View File
@@ -277,7 +277,7 @@ export const InviteUserButton = ({
if (!show) return null;
return (
(isLocalDev() || isNetBirdHosted()) && (
<UserInviteModal groups={groups}>
<Button
variant={"primary"}
@@ -285,9 +285,8 @@ export const InviteUserButton = ({
disabled={!permission.users.create}
>
<MailPlus size={16} />
Invite User
{isNetBirdHosted() ? "Invite User" : "Create User"}
</Button>
</UserInviteModal>
)
);
};