diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index dce76d406..8f0164d0d 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -75,6 +75,13 @@ "modal_add_api_token_copy_warning": "Please copy and save the API token below now. You won't be able to see it again.", "modal_rename_api_title": "Rename API token", "modal_add_user_title": "Add new user", + "modal_add_new_device_title": "Add new device", + "modal_add_new_device_subtitle": "Device activation settings", + "modal_add_new_device_subtitle_description": "Choose how to deliver the activation token and configure IP assignment for the user's device.", + "modal_add_new_device_choice_email_title": "Send token by email", + "modal_add_new_device_choice_email_content": "The device activation token will be sent to the user's email. You can use the one specified in the user's profile or enter an email manually.", + "modal_add_new_device_choice_manual_title": "Deliver token by yourself", + "modal_add_new_device_choice_manual_content": "At the next step, you will receive an activation token and a URL, which you can share with the user in any way that is convenient for you.", "modal_add_vector_destination_title": "Add Vector destination", "modal_add_logstash_destination_title": "Add Logstash destination", "modal_logstash_destination_title": "Logstash", @@ -123,6 +130,7 @@ "modal_ce_webhook_events_title": "Trigger events", "modal_ce_webhook_events_text": "", "modal_assign_users_groups_title": "Assign groups to selected users", + "modal_add_new_device_delivery_title": "Share activation credentials", "modal_assign_user_ip_title": "{firstName} {lastName} IP settings", "modal_assign_user_ip_title_fallback": "IP settings", "modal_assign_user_ip_assignment_mode_title": "Single IP Assignment", diff --git a/web/messages/en/users.json b/web/messages/en/users.json index 3e78b582b..6a7c6a49b 100644 --- a/web/messages/en/users.json +++ b/web/messages/en/users.json @@ -24,6 +24,7 @@ "users_row_menu_edit": "Edit details", "users_row_menu_change_password": "Change password", "users_row_menu_edit_groups": "Edit groups", + "user_row_menu_add_new_device": "Add new device", "users_row_menu_initiate_self_enrollment": "Initiate self-enrollment", "users_row_menu_ip_settings": "User devices IP settings", "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 55f99403b..bbd6b2bd3 100644 --- a/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx +++ b/web/src/pages/UsersOverviewPage/UsersOverviewPage.tsx @@ -6,6 +6,7 @@ import { AddAuthKeyModal } from '../../shared/components/modals/AddAuthKeyModal/ import { ChangePasswordModal } from '../../shared/components/modals/ChangePasswordModal/ChangePasswordModal'; import { TableSkeleton } from '../../shared/components/skeleton/TableSkeleton/TableSkeleton'; import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout'; +import { AddNewDeviceModal } from './modals/AddNewDeviceModal/AddNewDeviceModal'; import { AddUserModal } from './modals/AddUserModal/AddUserModal'; import { AssignUserIPModal } from './modals/AssignUserIPModal/AssignUserIPModal'; import { AssignUsersToGroupsModal } from './modals/AssignUsersToGroupsModal/AssignUsersToGroupsModal'; @@ -23,6 +24,7 @@ export const UsersOverviewPage = () => { + diff --git a/web/src/pages/UsersOverviewPage/UsersTable.tsx b/web/src/pages/UsersOverviewPage/UsersTable.tsx index 26545dcb4..6bed2b95f 100644 --- a/web/src/pages/UsersOverviewPage/UsersTable.tsx +++ b/web/src/pages/UsersOverviewPage/UsersTable.tsx @@ -388,6 +388,19 @@ export const UsersTable = () => { ], }); } + if (rowData.enrolled) { + menuItems.splice(1, 0, { + items: [ + { + text: m.user_row_menu_add_new_device(), + icon: IconKind.AddDevice, + onClick: () => { + openModal(ModalName.AddNewDevice, rowData); + }, + }, + ], + }); + } return ( diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx new file mode 100644 index 000000000..a0750fde9 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/AddNewDeviceModal.tsx @@ -0,0 +1,224 @@ +import './style.scss'; +import { useStore } from '@tanstack/react-form'; +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 type { StartEnrollmentResponse, User } from '../../../../shared/api/types'; +import { Controls } from '../../../../shared/components/Controls/Controls'; +import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; +import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; +import { Fold } from '../../../../shared/defguard-ui/components/Fold/Fold'; +import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; +import { SectionSelect } from '../../../../shared/defguard-ui/components/SectionSelect/SectionSelect'; +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 { useApp } from '../../../../shared/hooks/useApp'; +import { DeliverTokenStep } from './steps/DeliverTokenStep/DeliverTokenStep'; + +const modalName = ModalName.AddNewDevice; + +type DeliveryMethod = 'email' | 'manual'; + +export const AddNewDeviceModal = () => { + const [isOpen, setOpen] = useState(false); + const [user, setUser] = useState(null); + const [enrollmentData, setEnrollmentData] = useState( + null, + ); + + const handleAfterClose = () => { + setUser(null); + setEnrollmentData(null); + }; + + useEffect(() => { + const openSub = subscribeOpenModal(modalName, (data) => { + setUser(data); + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalName, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + + return ( + closeModal(modalName)} + afterClose={handleAfterClose} + > + {isPresent(user) && !isPresent(enrollmentData) && ( + + )} + {isPresent(enrollmentData) && } + + ); +}; + +const EnrollmentChoice = ({ + user, + onEnrollmentReady, +}: { + user: User; + onEnrollmentReady: (data: StartEnrollmentResponse) => void; +}) => { + const smtpEnabled = useApp((s) => s.appInfo.smtp_enabled); + const [selected, setSelected] = useState(() => + smtpEnabled ? 'email' : 'manual', + ); + + const { mutateAsync: startClientActivation } = useMutation({ + mutationFn: api.user.startClientActivation, + onError: (error) => { + Snackbar.error(m.failed_to_start_enrollment()); + console.error(error); + }, + }); + + const formSchema = useMemo( + () => + z + .object({ + email: z.string(), + }) + .superRefine((values, ctx) => { + if (selected === 'email') { + 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, + }); + } + } + }), + [selected], + ); + + const form = useAppForm({ + defaultValues: { + email: user.email ?? '', + }, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: async ({ value }) => { + if (selected === 'manual') { + const { data } = await startClientActivation({ + username: user.username, + send_enrollment_notification: false, + }); + onEnrollmentReady(data); + } else { + await startClientActivation({ + username: user.username, + send_enrollment_notification: true, + email: value.email, + }); + Snackbar.success(m.sucessfull_enrollment_email()); + closeModal(modalName); + } + }, + }); + const isSubmitting = useStore(form.store, (s) => s.isSubmitting); + + return ( + <> +
+ {m.modal_add_new_device_subtitle()} + + + {m.modal_add_new_device_subtitle_description()} + +
+ +
{ + e.preventDefault(); + e.stopPropagation(); + form.handleSubmit(); + }} + > + + { + if (smtpEnabled) setSelected('email'); + }} + > + + + + {(field) => } + + + + + setSelected('manual')} + /> + + +
+
+
+
+
+ + ); +}; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx new file mode 100644 index 000000000..493952fc1 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/DeliverTokenStep.tsx @@ -0,0 +1,46 @@ +import { m } from '../../../../../../paraglide/messages'; +import type { StartEnrollmentResponse } from '../../../../../../shared/api/types'; +import { Controls } from '../../../../../../shared/components/Controls/Controls'; +import { Button } from '../../../../../../shared/defguard-ui/components/Button/Button'; +import { CopyField } from '../../../../../../shared/defguard-ui/components/CopyField/CopyField'; +import { SizedBox } from '../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { ThemeSpacing } from '../../../../../../shared/defguard-ui/types'; +import { closeModal } from '../../../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../../../shared/hooks/modalControls/modalTypes'; +import './style.scss'; + +type Props = { + enrollmentData: StartEnrollmentResponse; +}; + +export const DeliverTokenStep = ({ enrollmentData }: Props) => { + return ( +
+
+

{m.modal_add_new_device_delivery_title()}

+ + + + +
+ + +
+
+
+
+ ); +}; diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/style.scss b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/style.scss new file mode 100644 index 000000000..34bd761c4 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/steps/DeliverTokenStep/style.scss @@ -0,0 +1,9 @@ +#add-new-device-delivery-step { + .section-title { + font: var(--t-body-sm-500); + } + + .share-credentials { + padding-top: var(--spacing-lg) 0; + } +} diff --git a/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss new file mode 100644 index 000000000..07c43da66 --- /dev/null +++ b/web/src/pages/UsersOverviewPage/modals/AddNewDeviceModal/style.scss @@ -0,0 +1,7 @@ +#add-user-modal { + form { + & > p { + font: var(--t-body-sm-500); + } + } +} diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui index 96b3e1f59..3552857ad 160000 --- a/web/src/shared/defguard-ui +++ b/web/src/shared/defguard-ui @@ -1 +1 @@ -Subproject commit 96b3e1f5922f43b4d9de3c1b210c36c02d592f46 +Subproject commit 3552857ad6f778a305bfabe464e7a0e14b19e515 diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index 3379b964d..56ad6bbb7 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -56,6 +56,7 @@ export const ModalName = { EditLogStreaming: 'editLogStreaming', DeleteLogStreaming: 'deleteLogStreaming', SelfEnrollmentToken: 'selfEnrollmentToken', + AddNewDevice: 'addNewDevice', AssignUserIP: 'assignUserIP', } as const; @@ -182,6 +183,10 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ name: z.literal(ModalName.LicenseExpired), data: z.custom(), }), + z.object({ + name: z.literal(ModalName.AddNewDevice), + data: z.custom(), + }), z.object({ name: z.literal(ModalName.AssignUserIP), data: z.custom(), diff --git a/web/src/shared/hooks/modalControls/types.ts b/web/src/shared/hooks/modalControls/types.ts index a67e8f1f7..af13d3ea4 100644 --- a/web/src/shared/hooks/modalControls/types.ts +++ b/web/src/shared/hooks/modalControls/types.ts @@ -65,6 +65,10 @@ export interface OpenEnrollmentTokenModal { enrollmentResponse: StartEnrollmentResponse; } +export interface OpenAddNewDeviceModal { + user: User; +} + export interface OpenCEWebhookModal { webhook?: Webhook; }