diff --git a/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/CreateUserCredentialModal.tsx b/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/CreateUserCredentialModal.tsx new file mode 100755 index 000000000..d9b921f4e --- /dev/null +++ b/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/CreateUserCredentialModal.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect } from 'react'; +import { useFormik } from 'formik'; +import * as yup from 'yup'; +import { Systems as Hooks } from '@tapis/tapisui-hooks'; +import { useTapisConfig } from '@tapis/tapisui-hooks'; +import { SystemsApi } from '@tapis/tapis-typescript-systems'; + +import { + Modal, + ModalHeader, + ModalBody, + ModalFooter, + Button, + FormGroup, + Label, + Input, + FormFeedback, + Alert, +} from 'reactstrap'; + +type CreateUserCredentialModalProps = { + toggle: () => void; + isOpen: boolean; + effectiveUserId: string; +}; + +interface FormData { + systemId: string; + publicKey: string; + privateKey: string; + loginUser?: string; +} + +// Custom transformation to ensure privateKey is a one-liner +const transformPrivateKey = (value: string) => value.replace(/\n/gm, '\\n'); + +const schema = yup.object().shape({ + systemId: yup + .string() + .required('System ID is required') + .min(3, 'System ID must be at least 3 characters') + .max(50, 'System ID must not exceed 50 characters') + .matches( + /^[a-zA-Z0-9-_.]+$/, + 'System ID can only contain letters, numbers, hyphens, underscores, and periods' + ), + publicKey: yup + .string() + .required('Public key is required') + .min(20, 'Public key must be at least 20 characters') + .max(4096, 'Public key must not exceed 4096 characters') + .test('is-valid-public-key', 'Invalid SSH public key format', (value) => { + return /^(ssh-rsa|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)\s+[A-Za-z0-9+/]+[=]{0,3}(\s+.*)?$/.test( + value! + ); + }), + privateKey: yup + .string() + .required('Private key is required') + .min(20, 'Private key must be at least 20 characters') + .max(4096, 'Private key must not exceed 4096 characters') + .transform(transformPrivateKey), + loginUser: yup + .string() + .optional() + .max(32, 'Login user must not exceed 32 characters') + .matches( + /^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$/, + 'Invalid login user format' + ), +}); + +const CreateUserCredentialModal: React.FC = ({ + toggle, + isOpen, + effectiveUserId, +}) => { + const [submitError, setSubmitError] = useState(null); + const { claims } = useTapisConfig(); + const userName = claims['tapis/username']; + + const { create, isLoading, isSuccess, error } = Hooks.useCreateCredential(); + + useEffect(() => { + if (error) { + setSubmitError( + error.message || 'An error occurred while creating the credential' + ); + } + }, [error]); + + const formik = useFormik({ + initialValues: { + systemId: '', + publicKey: '', + privateKey: '', + loginUser: '', + }, + validationSchema: schema, + onSubmit: async (values, { setSubmitting, resetForm }) => { + setSubmitError(null); + + console.log('Submitting form with values:', values); + + try { + create({ + systemId: values.systemId, + userName: userName, + reqUpdateCredential: { + publicKey: values.publicKey, + privateKey: transformPrivateKey(values.privateKey), + loginUser: values.loginUser, + }, + }); + console.log('Credential created successfully'); + resetForm(); + } catch (error) { + console.error('Error creating user credential:', error); + setSubmitError('Failed to create user credential. Please try again.'); + } finally { + setSubmitting(false); + } + }, + }); + + useEffect(() => { + if (effectiveUserId === userName) { + formik.setFieldValue('loginUser', userName); + } else { + formik.setFieldValue('loginUser', ''); + } + }, [effectiveUserId, userName]); + + return ( + + Create User Credential + + {submitError && {submitError}} +
+ + + + {formik.errors.systemId && formik.touched.systemId && ( + {formik.errors.systemId} + )} + + + + + {formik.errors.publicKey && formik.touched.publicKey && ( + {formik.errors.publicKey} + )} + + + + + {formik.errors.privateKey && formik.touched.privateKey && ( + {formik.errors.privateKey} + )} + + + + + {formik.errors.loginUser && formik.touched.loginUser && ( + {formik.errors.loginUser} + )} + + + + + +
+
+
+ ); +}; + +export default CreateUserCredentialModal; diff --git a/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/index.ts b/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/index.ts new file mode 100755 index 000000000..6a87c579d --- /dev/null +++ b/src/app/Systems/_components/SystemToolbar/CreateUserCredentialModal/index.ts @@ -0,0 +1,2 @@ +import CreateUserCredentialModal from './CreateUserCredentialModal'; +export default CreateUserCredentialModal; diff --git a/src/app/Systems/_components/SystemToolbar/SystemToolbar.tsx b/src/app/Systems/_components/SystemToolbar/SystemToolbar.tsx index 5cc1946b4..725718110 100644 --- a/src/app/Systems/_components/SystemToolbar/SystemToolbar.tsx +++ b/src/app/Systems/_components/SystemToolbar/SystemToolbar.tsx @@ -6,6 +6,7 @@ import { useLocation } from 'react-router-dom'; import CreateSystemModal from './CreateSystemModal'; import DeleteSystemModal from './DeleteSystemModal'; import UndeleteSystemModal from './UndeleteSystemModal'; +import CreateUserCredentialModal from './CreateUserCredentialModal'; type ToolbarButtonProps = { text: string; @@ -47,6 +48,7 @@ const SystemToolbar: React.FC = () => { const toggle = () => { setModal(undefined); }; + // Handling of the modal state return (
{pathname && ( @@ -72,12 +74,26 @@ const SystemToolbar: React.FC = () => { onClick={() => setModal('undeletesystem')} aria-label="undeleteSystem" /> - + {/* New button for creating user credentials */} + setModal('createusercredential')} + aria-label="createUserCredential" + /> + {/* Conditionally rendered modals */} {modal === 'createsystem' && } {modal === 'deletesystem' && } {modal === 'undeletesystem' && ( )} + {modal === 'createusercredential' && ( + + )}
)}