diff --git a/web/messages/en/common.json b/web/messages/en/common.json
index 95fc8c9d37..6c50778f35 100644
--- a/web/messages/en/common.json
+++ b/web/messages/en/common.json
@@ -50,5 +50,7 @@
"search_empty_common_title": "Nothing is found.",
"search_empty_common_subtitle": "Try adjusting your filters or search terms.",
"search_users": "Find users",
- "select_users_all": "All users"
+ "select_users_all": "All users",
+ "sucessfull_enrollment_email": "Enrollment email sent successfully",
+ "failed_to_start_enrollment": "Failed to start enrollment"
}
diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json
index 58f0504d99..69bc3657b6 100644
--- a/web/messages/en/modal.json
+++ b/web/messages/en/modal.json
@@ -62,7 +62,7 @@
"modal_add_user_device_manual_download_actions_download_all": "All locations",
"modal_add_user_enrollment_details": "Enrollment details",
"modal_add_user_enrollment_explain": "Provide the user with their token and URL in a convenient manner so they can complete the enrollment process independently.",
- "modal_add_user_enrollment_form_label_url": "URL",
+ "modal_add_user_enrollment_form_label_instance_url": "Defguard instance URL",
"modal_add_user_enrollment_form_label_token": "Activation token",
"modal_add_user_enrollment_form_label_send": "Send enrollment details to user by email",
"modal_edit_user_device_title": "Edit device",
@@ -85,6 +85,7 @@
"modal_select_log_streaming_destination_title": "Select destination",
"modal_add_log_streaming_destination_title": "Add destination",
"modal_add_user_groups_title": "Assign user to group(s)",
+ "modal_initiate_self_enrollment_title": "Initiate self-enrollment for user",
"modal_add_user_enroll_title": "Start enrollment for user",
"modal_add_user_choice_enroll_title": "Add user with self-enrollment option",
"modal_add_user_choice_enroll_content": "User will be able to configure their account during the setup of their desktop client (e.g., set their own password, etc.).",
diff --git a/web/messages/en/users.json b/web/messages/en/users.json
index 6844063276..75741282ff 100644
--- a/web/messages/en/users.json
+++ b/web/messages/en/users.json
@@ -24,5 +24,6 @@
"users_row_menu_edit": "Edit details",
"users_row_menu_change_password": "Change password",
"users_row_menu_edit_groups": "Edit groups",
+ "users_row_menu_initiate_self_enrollment": "Initiate self-enrollment",
"modal_edit_user_groups_title": "Edit user groups"
}
diff --git a/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx b/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx
index f7f3fed6d9..6bfa2bd815 100644
--- a/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx
+++ b/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx
@@ -10,6 +10,7 @@ import { getUsersQueryOptions } from '../../shared/query';
import { AddUserModal } from './modals/AddUserModal/AddUserModal';
import { AssignUsersToGroupsModal } from './modals/AssignUsersToGroupsModal/AssignUsersToGroupsModal';
import { EditUserModal } from './modals/EditUserModal/EditUserModal';
+import { EnrollmentTokenModal } from './modals/EnrollmentTokenModal/EnrollmentTokenModal';
import { UsersTable } from './UsersTable';
export const UsersOverviewPage = () => {
@@ -23,6 +24,7 @@ export const UsersOverviewPage = () => {
+
diff --git a/web/src/pages/UsersOverviewPage/UsersTable.tsx b/web/src/pages/UsersOverviewPage/UsersTable.tsx
index ea9b3f5099..3521b40c29 100644
--- a/web/src/pages/UsersOverviewPage/UsersTable.tsx
+++ b/web/src/pages/UsersOverviewPage/UsersTable.tsx
@@ -26,8 +26,9 @@ import { Button } from '../../shared/defguard-ui/components/Button/Button';
import type { ButtonProps } from '../../shared/defguard-ui/components/Button/types';
import { EmptyState } from '../../shared/defguard-ui/components/EmptyState/EmptyState';
import { EmptyStateFlexible } from '../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible';
-import { Icon } from '../../shared/defguard-ui/components/Icon';
+import { Icon, IconKind } from '../../shared/defguard-ui/components/Icon';
import { IconButtonMenu } from '../../shared/defguard-ui/components/IconButtonMenu/IconButtonMenu';
+import type { MenuItemsGroup } from '../../shared/defguard-ui/components/Menu/types';
import { Search } from '../../shared/defguard-ui/components/Search/Search';
import { tableEditColumnSize } from '../../shared/defguard-ui/components/table/consts';
import { TableBody } from '../../shared/defguard-ui/components/table/TableBody/TableBody';
@@ -35,9 +36,11 @@ import { TableCell } from '../../shared/defguard-ui/components/table/TableCell/T
import { TableFlexCell } from '../../shared/defguard-ui/components/table/TableFlexCell/TableFlexCell';
import { TableRowContainer } from '../../shared/defguard-ui/components/table/TableRowContainer/TableRowContainer';
import { TableTop } from '../../shared/defguard-ui/components/table/TableTop/TableTop';
+import { Snackbar } from '../../shared/defguard-ui/providers/snackbar/snackbar';
import { isPresent } from '../../shared/defguard-ui/utils/isPresent';
import { openModal } from '../../shared/hooks/modalControls/modalsSubjects';
import { ModalName } from '../../shared/hooks/modalControls/modalTypes';
+import { useApp } from '../../shared/hooks/useApp';
import { getGroupsInfoQueryOptions } from '../../shared/query';
import { displayDate } from '../../shared/utils/displayDate';
import { useAddUserModal } from './modals/AddUserModal/useAddUserModal';
@@ -51,6 +54,7 @@ type RowData = UsersListItem;
const columnHelper = createColumnHelper();
export const UsersTable = ({ users }: Props) => {
+ const appInfo = useApp((s) => s.appInfo);
const reservedEmails = useMemo(() => users.map((u) => u.email.toLowerCase()), [users]);
const reservedUsernames = useMemo(() => users.map((u) => u.username), [users]);
@@ -235,109 +239,138 @@ export const UsersTable = ({ users }: Props) => {
enableResizing: false,
cell: (info) => {
const rowData = info.row.original;
- return (
-
- {
- openModal(ModalName.EditUserModal, {
- user: rowData,
- reservedEmails,
- reservedUsernames,
- });
- },
- },
- {
- text: m.users_row_menu_change_password(),
- icon: 'lock-open',
- testId: 'change-password',
- onClick: () => {
- openModal(ModalName.ChangePassword, {
- adminForm: true,
- user: rowData,
- });
- },
- },
- {
- text: m.users_row_menu_go_profile(),
- icon: 'profile',
- onClick: () => {
- navigate({
- to: '/user/$username',
- params: {
- username: rowData.username,
- },
- });
- },
- },
- {
- text: m.users_row_menu_edit_groups(),
- icon: 'add-group',
- testId: 'edit-groups',
- onClick: () => {
- useSelectionModal.setState({
- isOpen: true,
- options: groupsOptions,
- title: m.modal_edit_user_groups_title(),
- selected: new Set(rowData.groups),
- onSubmit: (selected) => {
- handleEditGroups(rowData, selected as string[]);
- },
- });
- },
- },
- ],
+
+ const menuItems: MenuItemsGroup[] = [
+ {
+ items: [
+ {
+ text: m.users_row_menu_edit(),
+ icon: 'edit',
+ onClick: () => {
+ openModal(ModalName.EditUserModal, {
+ user: rowData,
+ reservedEmails,
+ reservedUsernames,
+ });
},
- {
- items: [
- {
- text: m.users_row_menu_add_auth(),
- icon: 'key',
- onClick: () => {
- openModal(ModalName.AddAuthKey, {
- username: rowData.username,
- });
- },
- },
- ],
+ },
+ {
+ text: m.users_row_menu_change_password(),
+ icon: 'lock-open',
+ testId: 'change-password',
+ onClick: () => {
+ openModal(ModalName.ChangePassword, {
+ adminForm: true,
+ user: rowData,
+ });
},
- {
- items: [
- {
- text: rowData.is_active
- ? m.users_row_menu_disable()
- : m.users_row_menu_enable(),
- icon: rowData.is_active ? 'disabled' : 'check-circle',
- testId: 'change-account-status',
- onClick: () => {
- changeAccountActiveState({
- active: !rowData.is_active,
- username: rowData.username,
- });
- },
+ },
+ {
+ text: m.users_row_menu_go_profile(),
+ icon: 'profile',
+ onClick: () => {
+ navigate({
+ to: '/user/$username',
+ params: {
+ username: rowData.username,
},
- ],
+ });
},
- {
- items: [
- {
- text: m.users_row_menu_delete(),
- icon: 'delete',
- variant: 'danger',
- onClick: () => {
- deleteUser(rowData.username);
- },
+ },
+ {
+ text: m.users_row_menu_edit_groups(),
+ icon: 'add-group',
+ testId: 'edit-groups',
+ onClick: () => {
+ useSelectionModal.setState({
+ isOpen: true,
+ options: groupsOptions,
+ title: m.modal_edit_user_groups_title(),
+ selected: new Set(rowData.groups),
+ onSubmit: (selected) => {
+ handleEditGroups(rowData, selected as string[]);
},
- ],
+ });
},
- ]}
- />
+ },
+ ],
+ },
+ {
+ items: [
+ {
+ text: m.users_row_menu_add_auth(),
+ icon: 'key',
+ onClick: () => {
+ openModal(ModalName.AddAuthKey, {
+ username: rowData.username,
+ });
+ },
+ },
+ ],
+ },
+ {
+ items: [
+ {
+ text: rowData.is_active
+ ? m.users_row_menu_disable()
+ : m.users_row_menu_enable(),
+ icon: rowData.is_active ? 'disabled' : 'check-circle',
+ testId: 'change-account-status',
+ onClick: () => {
+ changeAccountActiveState({
+ active: !rowData.is_active,
+ username: rowData.username,
+ });
+ },
+ },
+ ],
+ },
+ {
+ items: [
+ {
+ text: m.users_row_menu_delete(),
+ icon: 'delete',
+ variant: 'danger',
+ onClick: () => {
+ deleteUser(rowData.username);
+ },
+ },
+ ],
+ },
+ ];
+
+ if (!rowData.enrolled) {
+ menuItems.splice(1, 0, {
+ items: [
+ {
+ text: m.users_row_menu_initiate_self_enrollment(),
+ icon: IconKind.AddUser,
+ onClick: () => {
+ api.user
+ .startEnrollment({
+ send_enrollment_notification: false,
+ username: rowData.username,
+ })
+ .then((response) => {
+ openModal(ModalName.SelfEnrollmentToken, {
+ user: rowData,
+ appInfo,
+ enrollmentResponse: response.data,
+ });
+ })
+ .catch((error) => {
+ Snackbar.error('Failed to initiate enrollment');
+ console.error(error);
+ });
+ },
+ },
+ ],
+ });
+ }
+
+ return (
+
+
);
},
@@ -352,6 +385,7 @@ export const UsersTable = ({ users }: Props) => {
groupsOptions,
handleEditGroups,
groups,
+ appInfo,
],
);
diff --git a/web/src/pages/UsersOverviewPage/modals/AddUserModal/AddUserModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddUserModal/AddUserModal.tsx
index 9f5dae9633..95b28196dc 100644
--- a/web/src/pages/UsersOverviewPage/modals/AddUserModal/AddUserModal.tsx
+++ b/web/src/pages/UsersOverviewPage/modals/AddUserModal/AddUserModal.tsx
@@ -149,7 +149,7 @@ const EnrollmentStep = () => {
@@ -358,13 +358,19 @@ const AddUserModalForm = () => {
data: { groups },
} = await api.group.getGroups();
if (enrollmentEnabled) {
- const enrollmentResponse = (
- await api.user.startEnrollment({
- send_enrollment_notification: false,
- username: created.username,
- })
- ).data;
- useAddUserModal.setState({ enrollResponse: enrollmentResponse });
+ try {
+ const enrollmentResponse = (
+ await api.user.startEnrollment({
+ send_enrollment_notification: false,
+ username: created.username,
+ })
+ ).data;
+ useAddUserModal.setState({ enrollResponse: enrollmentResponse, user: created });
+ } catch (error) {
+ console.error(m.failed_to_start_enrollment(), error);
+ useAddUserModal.setState({ isOpen: false });
+ return;
+ }
}
if (assignToGroups) {
useAddUserModal.setState({
diff --git a/web/src/pages/UsersOverviewPage/modals/EnrollmentTokenModal/EnrollmentTokenModal.tsx b/web/src/pages/UsersOverviewPage/modals/EnrollmentTokenModal/EnrollmentTokenModal.tsx
new file mode 100644
index 0000000000..0373d11340
--- /dev/null
+++ b/web/src/pages/UsersOverviewPage/modals/EnrollmentTokenModal/EnrollmentTokenModal.tsx
@@ -0,0 +1,184 @@
+import { useMutation } from '@tanstack/react-query';
+import { useEffect, useMemo, useState } from 'react';
+import z from 'zod';
+import { m } from '../../../../paraglide/messages';
+import api from '../../../../shared/api/api';
+import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText';
+import { Checkbox } from '../../../../shared/defguard-ui/components/Checkbox/Checkbox';
+import { CopyField } from '../../../../shared/defguard-ui/components/CopyField/CopyField';
+import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal';
+import { ModalControls } from '../../../../shared/defguard-ui/components/ModalControls/ModalControls';
+import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox';
+import { Snackbar } from '../../../../shared/defguard-ui/providers/snackbar/snackbar';
+import {
+ TextStyle,
+ ThemeSpacing,
+ ThemeVariable,
+} from '../../../../shared/defguard-ui/types';
+import { isPresent } from '../../../../shared/defguard-ui/utils/isPresent';
+import { useAppForm } from '../../../../shared/form';
+import { formChangeLogic } from '../../../../shared/formLogic';
+import {
+ closeModal,
+ subscribeCloseModal,
+ subscribeOpenModal,
+} from '../../../../shared/hooks/modalControls/modalsSubjects';
+import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes';
+import type { OpenEnrollmentTokenModal } from '../../../../shared/hooks/modalControls/types';
+
+const modalName = ModalName.SelfEnrollmentToken;
+
+type ModalData = OpenEnrollmentTokenModal;
+
+export const EnrollmentTokenModal = () => {
+ const [isOpen, setOpen] = useState(false);
+ const [modalData, setModalData] = useState(null);
+
+ useEffect(() => {
+ const openSub = subscribeOpenModal(modalName, (data) => {
+ setModalData(data);
+ setOpen(true);
+ });
+ const closeSub = subscribeCloseModal(modalName, () => setOpen(false));
+ return () => {
+ openSub.unsubscribe();
+ closeSub.unsubscribe();
+ };
+ }, []);
+
+ return (
+ setOpen(false)}
+ afterClose={() => {
+ setModalData(null);
+ }}
+ >
+ {isPresent(modalData) && }
+
+ );
+};
+
+const ModalContent = ({ user, appInfo, enrollmentResponse }: ModalData) => {
+ const [sendEmail, setSendEmail] = useState(false);
+
+ const { mutateAsync: sendEnrollmentEmail } = useMutation({
+ mutationFn: api.user.startEnrollment,
+ onSuccess: () => {
+ Snackbar.success(m.sucessfull_enrollment_email());
+ closeModal(modalName);
+ },
+ onError: (error) => {
+ Snackbar.error(m.failed_to_start_enrollment());
+ console.error(error);
+ },
+ });
+
+ const formSchema = useMemo(
+ () =>
+ z
+ .object({
+ email: z.string(),
+ })
+ .superRefine((values, ctx) => {
+ if (sendEmail) {
+ const result = z
+ .email(m.form_error_email())
+ .min(1, m.form_error_required())
+ .safeParse(values.email);
+ if (!result.success) {
+ ctx.addIssue({
+ code: 'custom',
+ path: ['email'],
+ message: result.error.issues[0].message,
+ });
+ }
+ }
+ }),
+ [sendEmail],
+ );
+
+ const form = useAppForm({
+ defaultValues: {
+ email: user.email ?? '',
+ },
+ validationLogic: formChangeLogic,
+ validators: {
+ onSubmit: formSchema,
+ onChange: formSchema,
+ },
+ onSubmit: async ({ value }) => {
+ await sendEnrollmentEmail({
+ username: user.username,
+ send_enrollment_notification: true,
+ email: value.email,
+ });
+ },
+ });
+
+ useEffect(() => {
+ if (!form.state.isPristine) {
+ form.validateAllFields('change');
+ }
+ }, [form.state.isPristine, form.validateAllFields]);
+
+ return (
+ <>
+
+
+ {m.modal_add_user_enrollment_details()}
+
+
+
+ {m.modal_add_user_enrollment_explain()}
+
+
+
+
+
+
+ {appInfo.smtp_enabled && (
+ <>
+
+
+ setSendEmail((s) => !s)}
+ />
+ {sendEmail && (
+ <>
+
+
+ {(field) => }
+
+ >
+ )}
+
+ >
+ )}
+
+ {() => (
+ closeModal(modalName),
+ }}
+ />
+ )}
+
+ >
+ );
+};
diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts
index 7e5e0ddbe5..8c1e6f155d 100644
--- a/web/src/shared/hooks/modalControls/modalTypes.ts
+++ b/web/src/shared/hooks/modalControls/modalTypes.ts
@@ -12,6 +12,7 @@ import type {
OpenEditDeviceModal,
OpenEditNetworkDeviceModal,
OpenEditUserModal,
+ OpenEnrollmentTokenModal,
OpenLicenseModal,
OpenNetworkDeviceConfigModal,
OpenNetworkDeviceTokenModal,
@@ -49,6 +50,7 @@ export const ModalName = {
AddLogStreaming: 'addLogStreaming',
EditLogStreaming: 'editLogStreaming',
DeleteLogStreaming: 'deleteLogStreaming',
+ SelfEnrollmentToken: 'selfEnrollmentToken',
} as const;
export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName];
@@ -99,6 +101,10 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [
name: z.literal(ModalName.EditUserModal),
data: z.custom(),
}),
+ z.object({
+ name: z.literal(ModalName.SelfEnrollmentToken),
+ data: z.custom(),
+ }),
z.object({
name: z.literal(ModalName.EditUserModal),
data: z.custom(),
diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts
index e01e845d85..bbd1f4f346 100644
--- a/web/src/shared/hooks/modalControls/types.ts
+++ b/web/src/shared/hooks/modalControls/types.ts
@@ -54,6 +54,14 @@ export interface OpenCEOpenIdClientModal {
reservedNames: string[];
}
+export interface OpenEnrollmentTokenModal {
+ user: User;
+ appInfo: {
+ smtp_enabled: boolean;
+ };
+ enrollmentResponse: StartEnrollmentResponse;
+}
+
export interface OpenCEWebhookModal {
webhook?: Webhook;
}