+ );
+}
\ No newline at end of file
diff --git a/src/auth/OIDCProvider.tsx b/src/auth/OIDCProvider.tsx
index a19d49e4..6314f0ae 100644
--- a/src/auth/OIDCProvider.tsx
+++ b/src/auth/OIDCProvider.tsx
@@ -104,7 +104,9 @@ export default function OIDCProvider({ children }: Props) {
// We bypass authentication for pages that do not require auth.
// E.g., when we just want to show installation steps for public.
// Or the instance setup wizard for first-time setup.
- if (path === "/install" || path === "/setup") return children;
+ // Or the invite acceptance page for new users.
+ if (path === "/install" || path === "/setup" || path?.startsWith("/invite"))
+ return children;
return mounted && providerConfig ? (
parseInt(p, 10) || 0);
+ const latestParts = latest.split(".").map((p) => parseInt(p, 10) || 0);
+
+ for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
+ const c = currentParts[i] || 0;
+ const l = latestParts[i] || 0;
+ if (l > c) return true;
+ if (l < c) return false;
+ }
+ return false;
+}
+
+export const NavigationVersionInfo = () => {
+ const { isNavigationCollapsed, mobileNavOpen } = useApplicationContext();
+
+ // Only show for self-hosted, not cloud
+ if (isNetBirdHosted()) return null;
+
+ return (
+
+ Invite link was deleted for {event.meta.username}{" "}
+ {event.meta.email}
+
+ );
+
/**
* Service User
*/
diff --git a/src/modules/users/UserInviteModal.tsx b/src/modules/users/UserInviteModal.tsx
index b3941579..62e3ff94 100644
--- a/src/modules/users/UserInviteModal.tsx
+++ b/src/modules/users/UserInviteModal.tsx
@@ -12,10 +12,11 @@ import {
import { notify } from "@components/Notification";
import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
-import { IconMailForward } from "@tabler/icons-react";
+import { SegmentedTabs } from "@components/SegmentedTabs";
+import { IconMailForward, IconLink, IconUserPlus } from "@tabler/icons-react";
import { useApiCall } from "@utils/api";
import { cn, validator } from "@utils/helpers";
-import { CopyIcon, MailIcon, User2 } from "lucide-react";
+import { AlarmClock, CopyIcon, MailIcon, User2 } from "lucide-react";
import Image from "next/image";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
@@ -25,28 +26,50 @@ import Avatar2 from "@/assets/avatars/030.jpg";
import Avatar3 from "@/assets/avatars/063.jpg";
import Avatar4 from "@/assets/avatars/086.jpg";
import { Group } from "@/interfaces/Group";
-import { Role, User } from "@/interfaces/User";
+import { Role, User, UserInviteCreateResponse } from "@/interfaces/User";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { UserRoleSelector } from "@/modules/users/UserRoleSelector";
-import {isNetBirdHosted} from "@utils/netbird";
+import { isNetBirdHosted } from "@utils/netbird";
+
+type UserCreationMode = "create" | "invite";
type Props = {
children: React.ReactNode;
groups?: Group[];
};
-const copyMessage = "Password was copied to your clipboard!";
+const passwordCopyMessage = "Password was copied to your clipboard!";
+const inviteLinkCopyMessage = "Invite link was copied to your clipboard!";
+
+type SuccessData =
+ | { type: "password"; user: User }
+ | { type: "invite"; invite: UserInviteCreateResponse };
export default function UserInviteModal({ children, groups }: Readonly) {
const [open, setOpen] = useState(false);
const [successModal, setSuccessModal] = useState(false);
- const [createdUser, setCreatedUser] = useState();
+ const [successData, setSuccessData] = useState(null);
const { mutate } = useSWRConfig();
- const [, copyToClipboard] = useCopyToClipboard(createdUser?.password);
- const handleOnSuccess = (user: User) => {
+ const isPasswordSuccess = successData?.type === "password";
+ const isInviteSuccess = successData?.type === "invite";
+
+ const getInviteFullUrl = () => {
+ if (!isInviteSuccess) return "";
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
+ return `${origin}/invite?token=${successData.invite.invite_link}`;
+ };
+
+ const getCopyValue = () => {
+ if (successData?.type === "password") return successData.user.password;
+ if (successData?.type === "invite") return getInviteFullUrl();
+ return undefined;
+ };
+ const [, copyToClipboard] = useCopyToClipboard(getCopyValue());
+
+ const handleUserCreated = (user: User) => {
if (user.password) {
- setCreatedUser(user);
+ setSuccessData({ type: "password", user });
setSuccessModal(true);
} else {
setOpen(false);
@@ -56,9 +79,22 @@ export default function UserInviteModal({ children, groups }: Readonly) {
}, 1000);
};
+ const handleInviteCreated = (invite: UserInviteCreateResponse) => {
+ setSuccessData({ type: "invite", invite });
+ setSuccessModal(true);
+ setTimeout(() => {
+ mutate("/users?service_user=false");
+ mutate("/users/invites");
+ }, 1000);
+ };
+
const handleCopyAndClose = () => {
- copyToClipboard(copyMessage).then(() => {
- setCreatedUser(undefined);
+ const message =
+ successData?.type === "password"
+ ? passwordCopyMessage
+ : inviteLinkCopyMessage;
+ copyToClipboard(message).then(() => {
+ setSuccessData(null);
setSuccessModal(false);
setOpen(false);
});
@@ -68,14 +104,18 @@ export default function UserInviteModal({ children, groups }: Readonly) {
<>
{children}
-
+ {
if (!open) {
- setCreatedUser(undefined);
+ setSuccessData(null);
}
setSuccessModal(open);
setOpen(open);
@@ -85,7 +125,7 @@ export default function UserInviteModal({ children, groups }: Readonly) {
onEscapeKeyDown={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
- maxWidthClass={"max-w-md"}
+ maxWidthClass={isInviteSuccess ? "max-w-xl" : "max-w-md"}
className={"mt-20"}
showClose={false}
>
@@ -93,20 +133,41 @@ export default function UserInviteModal({ children, groups }: Readonly) {
- User created successfully!
+ {isPasswordSuccess && "User created successfully!"}
+ {isInviteSuccess && "Invite link created!"}
- This password will not be shown again, so be sure to copy it
- and store in a secure location.
+ {isPasswordSuccess &&
+ "This password will not be shown again, so be sure to copy it and store in a secure location."}
+ {isInviteSuccess &&
+ "Share this link with the user. They will be able to set their own password."}