From c4704aa2985a803ef3ebe9e42749c1cf01e29dd9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 4 Apr 2023 23:03:48 -0400 Subject: [PATCH 01/81] fix(striker-ui): rename message id in manage manifest --- .../components/ManageManifest/ManageManifestPanel.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/striker-ui/components/ManageManifest/ManageManifestPanel.tsx b/striker-ui/components/ManageManifest/ManageManifestPanel.tsx index a6dbdc3f..4d5f673f 100644 --- a/striker-ui/components/ManageManifest/ManageManifestPanel.tsx +++ b/striker-ui/components/ManageManifest/ManageManifestPanel.tsx @@ -45,7 +45,7 @@ import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; -const MSG_ID_API = 'api'; +const MSG_ID_MANIFEST_API = 'api'; const getFormData = ( ...[{ target }]: DivFormEventHandlerParameters @@ -237,7 +237,7 @@ const ManageManifestPanel: FC = () => { api[method](url, body) .then(() => { - setMessage(MSG_ID_API, { + setMessage(MSG_ID_MANIFEST_API, { children: successMsg, }); }) @@ -245,7 +245,7 @@ const ManageManifestPanel: FC = () => { const emsg = handleAPIError(apiError); emsg.children = getErrorMsg(emsg.children); - setMessage(MSG_ID_API, emsg); + setMessage(MSG_ID_MANIFEST_API, emsg); }) .finally(() => { setIsSubmittingForm(false); From 9878d03ee5d5a64d306330e7a67df06d51b53253 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 4 Apr 2023 23:09:20 -0400 Subject: [PATCH 02/81] fix(striker-ui): add event handler append props in InputWithRef --- striker-ui/components/InputWithRef.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 7c0ad269..00faccd7 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -25,7 +25,9 @@ type InputWithRefOptionalPropsWithoutDefault< TypeName extends keyof MapToInputType, > = { inputTestBatch?: InputTestBatch; + onBlurAppend?: InputBaseProps['onBlur']; onFirstRender?: InputFirstRenderFunction; + onFocusAppend?: InputBaseProps['onFocus']; onUnmount?: () => void; valueKey?: CreateInputOnChangeHandlerOptions['valueKey']; }; @@ -69,7 +71,9 @@ const InputWithRef = forwardRef( { input, inputTestBatch, + onBlurAppend, onFirstRender, + onFocusAppend, onUnmount, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, valueKey, @@ -125,14 +129,20 @@ const InputWithRef = forwardRef( () => initOnBlur ?? (testInput && - (({ target: { value } }) => { + ((...args) => { + const { + 0: { + target: { value }, + }, + } = args; const isValid = testInput({ inputs: { [INPUT_TEST_ID]: { value } }, }); setIsInputValid(isValid); + onBlurAppend?.call(null, ...args); })), - [initOnBlur, testInput], + [initOnBlur, onBlurAppend, testInput], ); const onChange = useMemo( () => @@ -160,10 +170,11 @@ const InputWithRef = forwardRef( () => initOnFocus ?? (inputTestBatch && - (() => { + ((...args) => { inputTestBatch.defaults?.onSuccess?.call(null, { append: {} }); + onFocusAppend?.call(null, ...args); })), - [initOnFocus, inputTestBatch], + [initOnFocus, inputTestBatch, onFocusAppend], ); /** From 95ce723b861b639f1b9daffc5d83a5c7b35161c0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 4 Apr 2023 23:57:25 -0400 Subject: [PATCH 03/81] fix(striker-ui): revise GateForm to use form event --- striker-ui/components/GateForm.tsx | 367 +++++++++------------- striker-ui/components/PrepareHostForm.tsx | 23 +- striker-ui/types/GateForm.d.ts | 11 +- 3 files changed, 164 insertions(+), 237 deletions(-) diff --git a/striker-ui/components/GateForm.tsx b/striker-ui/components/GateForm.tsx index b72de4cf..49df89a1 100644 --- a/striker-ui/components/GateForm.tsx +++ b/striker-ui/components/GateForm.tsx @@ -1,7 +1,6 @@ -import { SxProps, Theme } from '@mui/material'; +import { Box, BoxProps, SxProps, Theme } from '@mui/material'; import { forwardRef, - useCallback, useImperativeHandle, useMemo, useRef, @@ -17,26 +16,22 @@ import InputWithRef, { InputForwardedRefContent } from './InputWithRef'; import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup'; import OutlinedInputWithLabel from './OutlinedInputWithLabel'; import Spinner from './Spinner'; -import { - buildPeacefulStringTestBatch, - createTestInputFunction, -} from '../lib/test_input'; +import { buildPeacefulStringTestBatch } from '../lib/test_input'; +import useFormUtils from '../hooks/useFormUtils'; const INPUT_ROOT_SX: SxProps = { width: '100%' }; -const IT_IDS = { - identifier: 'identifier', - passphrase: 'passphrase', -}; -const MESSAGE_KEY: GateFormMessageKey = { - accessError: 'accessError', - identifierInputError: 'identifierInputError', - passphraseInputError: 'passphraseInputError', -}; + +const INPUT_ID_PREFIX_GATE = 'gate-input'; + +const INPUT_ID_GATE_ID = `${INPUT_ID_PREFIX_GATE}-credential-id`; +const INPUT_ID_GATE_PASSPHRASE = `${INPUT_ID_PREFIX_GATE}-credential-passphrase`; + +const MSG_ID_GATE_ACCESS = 'access'; const GateForm = forwardRef( ( { - allowSubmit: isAllowSubmit = true, + formContainer: isFormContainer = true, gridProps: { columns: gridColumns = { xs: 1, sm: 2 }, layout, @@ -49,7 +44,8 @@ const GateForm = forwardRef( inputProps: identifierInputProps, ...restIdentifierOutlinedInputWithLabelProps } = {}, - identifierInputTestBatchBuilder: overwriteIdentifierInputTestBatch, + identifierInputTestBatchBuilder: + buildIdentifierInputTestBatch = buildPeacefulStringTestBatch, onIdentifierBlurAppend, onSubmit, onSubmitAppend, @@ -72,151 +68,83 @@ const GateForm = forwardRef( const inputPassphraseRef = useRef>({}); const messageGroupRef = useRef({}); - const [isInputIdentifierValid, setIsInputIdentifierValid] = - useState(false); - const [isInputPassphraseValid, setIsInputPassphraseValid] = - useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const setAccessErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.accessError, - message, - ); - }, - [], - ); - const setIdentifierInputErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.identifierInputError, - message, - ); - }, - [], - ); - const setPassphraseInputErrorMessage: GateFormMessageSetter = useCallback( - (message?) => { - messageGroupRef.current.setMessage?.call( - null, - MESSAGE_KEY.passphraseInputError, - message, - ); - }, - [], + const formUtils = useFormUtils( + [INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE], + messageGroupRef, ); + const { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + buildInputUnmountFunction, + isFormInvalid, + setMessage, + } = formUtils; - const messagesGroupSxDisplay = useMemo( - () => (isAllowSubmit ? undefined : 'none'), - [isAllowSubmit], - ); - const identifierInputTestBatch = useMemo( - () => - overwriteIdentifierInputTestBatch?.call( - null, - setIdentifierInputErrorMessage, - inputIdentifierRef.current, - ) ?? - buildPeacefulStringTestBatch( - identifierLabel, - () => { - setIdentifierInputErrorMessage(); - }, - { getValue: inputIdentifierRef.current.getValue }, - (message) => { - setIdentifierInputErrorMessage({ - children: message, - type: 'warning', - }); - }, - ), - [ - identifierLabel, - overwriteIdentifierInputTestBatch, - setIdentifierInputErrorMessage, - ], - ); - const inputTests: InputTestBatches = useMemo( - () => ({ - [IT_IDS.identifier]: identifierInputTestBatch, - [IT_IDS.passphrase]: buildPeacefulStringTestBatch( - passphraseLabel, - () => { - setPassphraseInputErrorMessage(); - }, - { getValue: inputPassphraseRef.current.getValue }, - (message) => { - setPassphraseInputErrorMessage({ - children: message, - type: 'warning', - }); - }, - ), - }), - [ - identifierInputTestBatch, - passphraseLabel, - setPassphraseInputErrorMessage, - ], - ); - const submitHandler: ContainedButtonProps['onClick'] = useMemo( + const submitHandler: DivFormEventHandler = useMemo( () => onSubmit ?? ((...args) => { - setAccessErrorMessage(); + const { 0: event } = args; + + event.preventDefault(); + + setMessage(MSG_ID_GATE_ACCESS); setIsSubmitting(true); onSubmitAppend?.call( null, inputIdentifierRef.current, inputPassphraseRef.current, - setAccessErrorMessage, + (message?) => { + setMessage(MSG_ID_GATE_ACCESS, message); + }, setIsSubmitting, messageGroupRef.current, ...args, ); }), - [onSubmit, onSubmitAppend, setAccessErrorMessage], + [onSubmit, onSubmitAppend, setMessage], ); + const submitElement = useMemo( () => isSubmitting ? ( ) : ( - + {submitLabel} ), - [ - isInputIdentifierValid, - isInputPassphraseValid, - isSubmitting, - submitHandler, - submitLabel, - ], - ); - const submitGrid = useMemo( - () => - isAllowSubmit - ? { - children: submitElement, - sm: 2, - } - : undefined, - [isAllowSubmit, submitElement], + [isFormInvalid, isSubmitting, submitLabel], ); - const testInput = useMemo( - () => createTestInputFunction(inputTests), - [inputTests], - ); + const submitAreaGridLayout = useMemo(() => { + const result: GridLayout = {}; + + if (isFormContainer) { + result['gate-cell-message-group'] = { + children: , + sm: 2, + }; + result['gate-cell-submit'] = { children: submitElement, sm: 2 }; + } + + return result; + }, [isFormContainer, submitElement]); + + const containerProps = useMemo(() => { + const result: BoxProps = {}; + + if (isFormContainer) { + result.component = 'form'; + result.onSubmit = submitHandler; + } + + return result; + }, [isFormContainer, submitHandler]); useImperativeHandle(ref, () => ({ get: () => ({ @@ -232,90 +160,105 @@ const GateForm = forwardRef( })); return ( - { - const { - target: { value }, - } = event; - - const valid = testInput({ - inputs: { [IT_IDS.identifier]: { value } }, - }); - setIsInputIdentifierValid(valid); - - onIdentifierBlurAppend?.call(null, event); - }, - onFocus: () => { - setIdentifierInputErrorMessage(); - }, - ...identifierInputProps, - }} - label={identifierLabel} - {...restIdentifierOutlinedInputWithLabelProps} - /> - } - ref={inputIdentifierRef} - /> - ), - }, - 'credential-passphrase': { - children: ( - { - const valid = testInput({ - inputs: { [IT_IDS.passphrase]: { value } }, - }); - setIsInputPassphraseValid(valid); - }, - onFocus: () => { - setPassphraseInputErrorMessage(); - }, - type: INPUT_TYPES.password, - ...passphraseInputProps, - }} - label={passphraseLabel} - {...restPassphraseOutlinedInputWithLabelProps} - /> - } - ref={inputPassphraseRef} - /> - ), - }, - 'credential-message-group': { - children: , - sm: 2, - sx: { display: messagesGroupSxDisplay }, - }, - 'credential-submit': submitGrid, - }} - spacing={gridSpacing} - {...restGridProps} - /> + + + } + inputTestBatch={buildIdentifierInputTestBatch( + identifierLabel, + () => { + setMessage(INPUT_ID_GATE_ID); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(INPUT_ID_GATE_ID), + }, + (message) => { + setMessage(INPUT_ID_GATE_ID, { children: message }); + }, + )} + onBlurAppend={(...args) => { + onIdentifierBlurAppend?.call(null, ...args); + }} + onFirstRender={buildInputFirstRenderFunction( + INPUT_ID_GATE_ID, + )} + onUnmount={buildInputUnmountFunction(INPUT_ID_GATE_ID)} + ref={inputIdentifierRef} + required + /> + ), + }, + 'gate-input-cell-credential-passphrase': { + children: ( + + } + inputTestBatch={buildPeacefulStringTestBatch( + passphraseLabel, + () => { + setMessage(INPUT_ID_GATE_PASSPHRASE); + }, + { + onFinishBatch: buildFinishInputTestBatchFunction( + INPUT_ID_GATE_PASSPHRASE, + ), + }, + (message) => { + setMessage(INPUT_ID_GATE_PASSPHRASE, { + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction( + INPUT_ID_GATE_PASSPHRASE, + )} + onUnmount={buildInputUnmountFunction( + INPUT_ID_GATE_PASSPHRASE, + )} + ref={inputPassphraseRef} + required + /> + ), + }, + ...submitAreaGridLayout, + }} + spacing={gridSpacing} + {...restGridProps} + /> + ); }, ); GateForm.displayName = 'GateForm'; +export { INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE }; + export default GateForm; diff --git a/striker-ui/components/PrepareHostForm.tsx b/striker-ui/components/PrepareHostForm.tsx index 8dc1cbea..7f416cfd 100644 --- a/striker-ui/components/PrepareHostForm.tsx +++ b/striker-ui/components/PrepareHostForm.tsx @@ -58,7 +58,6 @@ const PrepareHostForm: FC = () => { const { protect } = useProtect(); const confirmDialogRef = useRef({}); - const gateFormRef = useRef({}); const inputEnterpriseKeyRef = useRef>({}); const inputHostNameRef = useRef>({}); const inputRedhatPassword = useRef>({}); @@ -199,7 +198,7 @@ const PrepareHostForm: FC = () => { const accessSection = useMemo( () => ( { }, }, }} - identifierInputTestBatchBuilder={(setMessage) => - buildIPAddressTestBatch( - HOST_IP_LABEL, - () => { - setMessage(); - }, - undefined, - (message) => { - setMessage({ children: message, type: 'warning' }); - }, - ) - } + identifierInputTestBatchBuilder={buildIPAddressTestBatch} identifierLabel={HOST_IP_LABEL} onIdentifierBlurAppend={({ target: { value } }) => { if (connectedHostIPAddress) { @@ -291,17 +279,16 @@ const PrepareHostForm: FC = () => { } }, ) - .catch((error) => { - const errorMessage = handleAPIError(error); + .catch((apiError) => { + const emsg = handleAPIError(apiError); - setMessage?.call(null, errorMessage); + setMessage?.call(null, emsg); }) .finally(() => { setIsSubmitting(false); }); }} passphraseLabel="Host root password" - ref={gateFormRef} submitLabel="Test access" /> ), diff --git a/striker-ui/types/GateForm.d.ts b/striker-ui/types/GateForm.d.ts index 5505c16b..f0f833e5 100644 --- a/striker-ui/types/GateForm.d.ts +++ b/striker-ui/types/GateForm.d.ts @@ -15,21 +15,18 @@ type GateFormSubmitHandler = ( setMessage: GateFormMessageSetter, setIsSubmitting: GateFormSubmittingSetter, messageGroupContent: import('../components/MessageGroup').MessageGroupForwardedRefContent, - ...args: Parameters + ...args: Parameters ) => void; type GateFormOptionalProps = { - allowSubmit?: boolean; + formContainer?: boolean; gridProps?: Partial; identifierOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps >; - identifierInputTestBatchBuilder?: ( - setMessage: GateFormMessageSetter, - identifierContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, - ) => ReturnType; + identifierInputTestBatchBuilder?: BuildInputTestBatchFunction; onIdentifierBlurAppend?: import('../components/OutlinedInput').OutlinedInputProps['onBlur']; - onSubmit?: ContainedButtonProps['onClick']; + onSubmit?: DivFormEventHandler; onSubmitAppend?: GateFormSubmitHandler; passphraseOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps From f8b6520da8a7ea27d90d1265ff158a2e8bca0aef Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 5 Apr 2023 17:18:51 -0400 Subject: [PATCH 04/81] fix(striker-ui): inherit colour in input test messages --- striker-ui/lib/test_input/buildDomainTestBatch.tsx | 4 ++-- .../lib/test_input/buildPeacefulStringTestBatch.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/striker-ui/lib/test_input/buildDomainTestBatch.tsx b/striker-ui/lib/test_input/buildDomainTestBatch.tsx index 9ed17475..0d0eeb7d 100644 --- a/striker-ui/lib/test_input/buildDomainTestBatch.tsx +++ b/striker-ui/lib/test_input/buildDomainTestBatch.tsx @@ -21,8 +21,8 @@ const buildDomainTestBatch: BuildInputTestBatchFunction = ( onDomainTestFailure( <> {inputName} can only contain lowercase alphanumeric, hyphen ( - - ), and dot () characters. + + ), and dot () characters. , ...args, ); diff --git a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx index e05e0fa2..ab633e7f 100644 --- a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx +++ b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx @@ -25,12 +25,12 @@ const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = ( onTestPeacefulStringFailureAppend( <> {inputName} cannot contain single-quote ( - - ), double-quote ( - ), slash ( - ), backslash ( - ), angle brackets ( - ), curly brackets ( + + ), double-quote ( + ), slash ( + ), backslash ( + ), angle brackets ( + ), curly brackets ( ). , ...args, From 305968e5e999e899dce81b64068b6e91bf116bb2 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 5 Apr 2023 22:23:52 -0400 Subject: [PATCH 05/81] fix(striker-ui): don't reset (re-render) form element in GateForm --- striker-ui/components/GateForm.tsx | 48 +++++++++++++----------------- striker-ui/types/GateForm.d.ts | 8 ++--- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/striker-ui/components/GateForm.tsx b/striker-ui/components/GateForm.tsx index 49df89a1..7ec49b7e 100644 --- a/striker-ui/components/GateForm.tsx +++ b/striker-ui/components/GateForm.tsx @@ -38,17 +38,19 @@ const GateForm = forwardRef( spacing: gridSpacing = '1em', ...restGridProps } = {}, + identifierId = INPUT_ID_GATE_ID, + identifierInputTestBatchBuilder: + buildIdentifierInputTestBatch = buildPeacefulStringTestBatch, identifierLabel, identifierOutlinedInputWithLabelProps: { formControlProps: identifierFormControlProps = {}, inputProps: identifierInputProps, ...restIdentifierOutlinedInputWithLabelProps } = {}, - identifierInputTestBatchBuilder: - buildIdentifierInputTestBatch = buildPeacefulStringTestBatch, onIdentifierBlurAppend, onSubmit, onSubmitAppend, + passphraseId = INPUT_ID_GATE_PASSPHRASE, passphraseLabel, passphraseOutlinedInputWithLabelProps: { formControlProps: passphraseFormControlProps = {}, @@ -56,6 +58,8 @@ const GateForm = forwardRef( ...restPassphraseOutlinedInputWithLabelProps } = {}, submitLabel, + // Props that depend on others. + allowSubmit: isAllowSubmit = isFormContainer, }, ref, ) => { @@ -94,13 +98,10 @@ const GateForm = forwardRef( setIsSubmitting(true); onSubmitAppend?.call( null, - inputIdentifierRef.current, - inputPassphraseRef.current, (message?) => { setMessage(MSG_ID_GATE_ACCESS, message); }, setIsSubmitting, - messageGroupRef.current, ...args, ); }), @@ -124,7 +125,7 @@ const GateForm = forwardRef( const submitAreaGridLayout = useMemo(() => { const result: GridLayout = {}; - if (isFormContainer) { + if (isAllowSubmit) { result['gate-cell-message-group'] = { children: , sm: 2, @@ -133,7 +134,7 @@ const GateForm = forwardRef( } return result; - }, [isFormContainer, submitElement]); + }, [isAllowSubmit, submitElement]); const containerProps = useMemo(() => { const result: BoxProps = {}; @@ -173,7 +174,7 @@ const GateForm = forwardRef( ...restIdentifierFormControlProps, sx: { ...INPUT_ROOT_SX, ...identifierSx }, }} - id={INPUT_ID_GATE_ID} + id={identifierId} inputProps={identifierInputProps} label={identifierLabel} {...restIdentifierOutlinedInputWithLabelProps} @@ -182,23 +183,21 @@ const GateForm = forwardRef( inputTestBatch={buildIdentifierInputTestBatch( identifierLabel, () => { - setMessage(INPUT_ID_GATE_ID); + setMessage(identifierId); }, { onFinishBatch: - buildFinishInputTestBatchFunction(INPUT_ID_GATE_ID), + buildFinishInputTestBatchFunction(identifierId), }, (message) => { - setMessage(INPUT_ID_GATE_ID, { children: message }); + setMessage(identifierId, { children: message }); }, )} onBlurAppend={(...args) => { onIdentifierBlurAppend?.call(null, ...args); }} - onFirstRender={buildInputFirstRenderFunction( - INPUT_ID_GATE_ID, - )} - onUnmount={buildInputUnmountFunction(INPUT_ID_GATE_ID)} + onFirstRender={buildInputFirstRenderFunction(identifierId)} + onUnmount={buildInputUnmountFunction(identifierId)} ref={inputIdentifierRef} required /> @@ -213,7 +212,7 @@ const GateForm = forwardRef( ...restPassphraseFormControlProps, sx: { ...INPUT_ROOT_SX, ...passphraseSx }, }} - id={INPUT_ID_GATE_PASSPHRASE} + id={passphraseId} inputProps={passphraseInputProps} label={passphraseLabel} type={INPUT_TYPES.password} @@ -223,25 +222,20 @@ const GateForm = forwardRef( inputTestBatch={buildPeacefulStringTestBatch( passphraseLabel, () => { - setMessage(INPUT_ID_GATE_PASSPHRASE); + setMessage(passphraseId); }, { - onFinishBatch: buildFinishInputTestBatchFunction( - INPUT_ID_GATE_PASSPHRASE, - ), + onFinishBatch: + buildFinishInputTestBatchFunction(passphraseId), }, (message) => { - setMessage(INPUT_ID_GATE_PASSPHRASE, { + setMessage(passphraseId, { children: message, }); }, )} - onFirstRender={buildInputFirstRenderFunction( - INPUT_ID_GATE_PASSPHRASE, - )} - onUnmount={buildInputUnmountFunction( - INPUT_ID_GATE_PASSPHRASE, - )} + onFirstRender={buildInputFirstRenderFunction(passphraseId)} + onUnmount={buildInputUnmountFunction(passphraseId)} ref={inputPassphraseRef} required /> diff --git a/striker-ui/types/GateForm.d.ts b/striker-ui/types/GateForm.d.ts index f0f833e5..51e4eaa6 100644 --- a/striker-ui/types/GateForm.d.ts +++ b/striker-ui/types/GateForm.d.ts @@ -10,24 +10,24 @@ type GateFormMessageSetter = ( type GateFormSubmittingSetter = (value: boolean) => void; type GateFormSubmitHandler = ( - identifierContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, - passphraseContent: import('../components/InputWithRef').InputForwardedRefContent<'string'>, setMessage: GateFormMessageSetter, setIsSubmitting: GateFormSubmittingSetter, - messageGroupContent: import('../components/MessageGroup').MessageGroupForwardedRefContent, ...args: Parameters ) => void; type GateFormOptionalProps = { + allowSubmit?: boolean; formContainer?: boolean; gridProps?: Partial; + identifierId?: string; + identifierInputTestBatchBuilder?: BuildInputTestBatchFunction; identifierOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps >; - identifierInputTestBatchBuilder?: BuildInputTestBatchFunction; onIdentifierBlurAppend?: import('../components/OutlinedInput').OutlinedInputProps['onBlur']; onSubmit?: DivFormEventHandler; onSubmitAppend?: GateFormSubmitHandler; + passphraseId?: string; passphraseOutlinedInputWithLabelProps?: Partial< import('../components/OutlinedInputWithLabel').OutlinedInputWithLabelProps >; From 7f826132a4a8b95745bd1263f1c930a79129d6d9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 6 Apr 2023 13:19:02 -0400 Subject: [PATCH 06/81] fix(striker-ui): pass input values to submit append in GateForm --- striker-ui/components/GateForm.tsx | 13 +++++++++++++ striker-ui/types/GateForm.d.ts | 2 ++ 2 files changed, 15 insertions(+) diff --git a/striker-ui/components/GateForm.tsx b/striker-ui/components/GateForm.tsx index 7ec49b7e..6a8e723c 100644 --- a/striker-ui/components/GateForm.tsx +++ b/striker-ui/components/GateForm.tsx @@ -96,8 +96,21 @@ const GateForm = forwardRef( setMessage(MSG_ID_GATE_ACCESS); setIsSubmitting(true); + + const { target } = event; + const { elements } = target as HTMLFormElement; + + const { value: identifierValue } = elements.namedItem( + INPUT_ID_GATE_ID, + ) as HTMLInputElement; + const { value: passphraseValue } = elements.namedItem( + INPUT_ID_GATE_PASSPHRASE, + ) as HTMLInputElement; + onSubmitAppend?.call( null, + identifierValue, + passphraseValue, (message?) => { setMessage(MSG_ID_GATE_ACCESS, message); }, diff --git a/striker-ui/types/GateForm.d.ts b/striker-ui/types/GateForm.d.ts index 51e4eaa6..41d817e5 100644 --- a/striker-ui/types/GateForm.d.ts +++ b/striker-ui/types/GateForm.d.ts @@ -10,6 +10,8 @@ type GateFormMessageSetter = ( type GateFormSubmittingSetter = (value: boolean) => void; type GateFormSubmitHandler = ( + identifier: string, + passphrase: string, setMessage: GateFormMessageSetter, setIsSubmitting: GateFormSubmittingSetter, ...args: Parameters From 4fc2655a4486f5c433c59d4a61e0e6d244b7f236 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 6 Apr 2023 14:49:18 -0400 Subject: [PATCH 07/81] fix(striker-ui): reflect GateForm changes in PrepareHostForm --- striker-ui/components/PrepareHostForm.tsx | 38 +++++++++-------------- striker-ui/types/APICommand.d.ts | 8 +++++ 2 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 striker-ui/types/APICommand.d.ts diff --git a/striker-ui/components/PrepareHostForm.tsx b/striker-ui/components/PrepareHostForm.tsx index 7f416cfd..0ea5017e 100644 --- a/striker-ui/components/PrepareHostForm.tsx +++ b/striker-ui/components/PrepareHostForm.tsx @@ -198,7 +198,7 @@ const PrepareHostForm: FC = () => { const accessSection = useMemo( () => ( { } }} onSubmitAppend={( - { getValue: getIdentifier }, - { getValue: getPassphrase }, - setMessage, - setIsSubmitting, + ipAddress, + password, + setGateMessage, + setGateIsSubmitting, ) => { - const identifierValue = getIdentifier?.call(null); - const passphraseValue = getPassphrase?.call(null); + const body = { ipAddress, password }; api - .put<{ - hostName: string; - hostOS: string; - hostUUID: string; - isConnected: boolean; - isInetConnected: boolean; - isOSRegistered: boolean; - }>('/command/inquire-host', { - ipAddress: identifierValue, - password: passphraseValue, - }) + .put( + '/command/inquire-host', + body, + ) .then( ({ data: { @@ -265,14 +257,14 @@ const PrepareHostForm: FC = () => { setIsShowRedhatSection(true); } - setConnectedHostIPAddress(identifierValue); - setConnectedHostPassword(passphraseValue); + setConnectedHostIPAddress(ipAddress); + setConnectedHostPassword(password); setConnectedHostUUID(hostUUID); setIsShowAccessSubmit(false); setIsShowOptionalSection(true); } else { - setMessage?.call(null, { + setGateMessage({ children: `Failed to establish a connection with the given host credentials.`, type: 'error', }); @@ -282,10 +274,10 @@ const PrepareHostForm: FC = () => { .catch((apiError) => { const emsg = handleAPIError(apiError); - setMessage?.call(null, emsg); + setGateMessage?.call(null, emsg); }) .finally(() => { - setIsSubmitting(false); + setGateIsSubmitting(false); }); }} passphraseLabel="Host root password" diff --git a/striker-ui/types/APICommand.d.ts b/striker-ui/types/APICommand.d.ts new file mode 100644 index 00000000..e8a084d6 --- /dev/null +++ b/striker-ui/types/APICommand.d.ts @@ -0,0 +1,8 @@ +type APICommandInquireHostResponseBody = { + hostName: string; + hostOS: string; + hostUUID: string; + isConnected: boolean; + isInetConnected: boolean; + isOSRegistered: boolean; +}; From b3f2644d077f61444d791e544cb30a2c3ca0c747 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 6 Apr 2023 15:24:47 -0400 Subject: [PATCH 08/81] fix: allow parameter to overwrite cgi input in Account->login --- Anvil/Tools/Account.pm | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Anvil/Tools/Account.pm b/Anvil/Tools/Account.pm index 68fbaf2d..e567e0d4 100644 --- a/Anvil/Tools/Account.pm +++ b/Anvil/Tools/Account.pm @@ -253,10 +253,14 @@ sub login my $self = shift; my $parameter = shift; my $anvil = $self->parent; - my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + my $password = $parameter->{password} // $anvil->data->{cgi}{password}{value}; + my $username = $parameter->{username} // $anvil->data->{cgi}{username}{value}; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->login()" }}); - if ((not $anvil->data->{cgi}{username}{value}) or (not $anvil->data->{cgi}{password}{value})) + if ((not $username) or (not $password)) { # The user forgot something... $anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }}); @@ -275,7 +279,7 @@ FROM WHERE user_algorithm != 'DELETED' AND - user_name = ".$anvil->Database->quote($anvil->data->{cgi}{username}{value})." + user_name = ".$anvil->Database->quote($username)." ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); @@ -309,7 +313,7 @@ AND # Test the passed-in password. my $test_password_answer = $anvil->Account->encrypt_password({ debug => 2, - password => $anvil->data->{cgi}{password}{value}, + password => $password, salt => $user_salt, algorithm => $user_algorithm, hash_count => $user_hash_count, @@ -345,7 +349,7 @@ AND }); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { session_uuid => $session_uuid }}); - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $anvil->data->{cgi}{username}{value} }}); + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $username }}); $anvil->Account->_write_cookies({ debug => $debug, hash => $session_hash, @@ -360,7 +364,7 @@ AND $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0184", variables => { user_agent => $ENV{HTTP_USER_AGENT} ? $ENV{HTTP_USER_AGENT} : "#!string!log_0185!#", source_ip => $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : "#!string!log_0185!#", - user => $anvil->data->{cgi}{username}{value}, + user => $username, }}); # Slow them down a bit... From aadc84ea70674e39681c3303a5f121ead0254f31 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 7 Apr 2023 01:11:55 -0400 Subject: [PATCH 09/81] feat(striker-ui-api): add initial login request handler --- .../src/lib/request_handlers/auth/login.ts | 108 ++++++++++++++++++ striker-ui-api/src/types/APIAuth.d.ts | 4 + 2 files changed, 112 insertions(+) create mode 100644 striker-ui-api/src/lib/request_handlers/auth/login.ts create mode 100644 striker-ui-api/src/types/APIAuth.d.ts diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts new file mode 100644 index 00000000..040e7052 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -0,0 +1,108 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { REP_PEACEFUL_STRING } from '../../consts/REG_EXP_PATTERNS'; + +import { dbQuery, sub } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr } from '../../shell'; + +export const login: RequestHandler = ( + request, + response, +) => { + const { + body: { password: rawPassword, username: rawUsername }, + } = request; + + const password = sanitize(rawPassword, 'string'); + const username = sanitize(rawUsername, 'string', { modifierType: 'sql' }); + + try { + assert( + REP_PEACEFUL_STRING.test(username), + `Username must be a peaceful string; got [${username}]`, + ); + + assert( + REP_PEACEFUL_STRING.test(password), + `Password must be a peaceful string; got [${password}]`, + ); + } catch (assertError) { + stderr( + `Assertion failed when attempting to authenticate; CAUSE: ${assertError}`, + ); + + response.status(400).send(); + + return; + } + + let rows: [ + userUuid: string, + userPasswordHash: string, + userSalt: string, + userAlgorithm: string, + userHashCount: string, + ][]; + + try { + rows = dbQuery(` + SELECT + user_uuid, + user_password_hash, + user_salt, + user_algorithm, + user_hash_count + FROM users + WHERE user_algorithm != 'DELETED' + AND user_name = '${username}'`).stdout; + } catch (queryError) { + stderr(`Failed to get user ${username}; CAUSE: ${queryError}`); + response.status(500).send(); + return; + } + + if (rows.length === 0) { + stderr(`No entry for user ${username} found`); + response.status(404).send(); + return; + } + + const { + 0: { 1: userPasswordHash, 2: userSalt, 3: userAlgorithm, 4: userHashCount }, + } = rows; + + let encryptResult: { + user_password_hash: string; + user_salt: string; + user_hash_count: number; + user_algorithm: string; + }; + + try { + encryptResult = sub('encrypt_password', { + subModuleName: 'Account', + subParams: { + algorithm: userAlgorithm, + hash_count: userHashCount, + password, + salt: userSalt, + }, + }).stdout; + } catch (subError) { + stderr(`Failed to login with username ${username}; CAUSE: ${subError}`); + response.status(500).send(); + return; + } + + const { user_password_hash: inputPasswordHash } = encryptResult; + + if (inputPasswordHash !== userPasswordHash) { + stderr(`Input and recorded password mismatched.`); + response.status(400).send(); + return; + } + + response.status(200).send(); +}; diff --git a/striker-ui-api/src/types/APIAuth.d.ts b/striker-ui-api/src/types/APIAuth.d.ts new file mode 100644 index 00000000..c85487c8 --- /dev/null +++ b/striker-ui-api/src/types/APIAuth.d.ts @@ -0,0 +1,4 @@ +type AuthLoginRequestBody = { + username: string; + password: string; +}; From f15391010c3f01dae2e11d4910fef9e0823a0ebd Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 10 Apr 2023 23:31:29 -0400 Subject: [PATCH 10/81] fix(striker-ui-api): organize routes into map --- striker-ui-api/src/app.ts | 9 ++---- .../src/lib/consts/API_ROOT_PATH.ts | 3 -- striker-ui-api/src/lib/rrouters.ts | 25 ++++++++++++++++ striker-ui-api/src/routes/index.ts | 30 +++++++++---------- striker-ui-api/src/types/MapToRouter.d.ts | 3 ++ 5 files changed, 46 insertions(+), 24 deletions(-) delete mode 100644 striker-ui-api/src/lib/consts/API_ROOT_PATH.ts create mode 100644 striker-ui-api/src/lib/rrouters.ts create mode 100644 striker-ui-api/src/types/MapToRouter.d.ts diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 1bb6396d..617bb09e 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,18 +1,15 @@ import cors from 'cors'; import express from 'express'; -import path from 'path'; - -import API_ROOT_PATH from './lib/consts/API_ROOT_PATH'; import routes from './routes'; +import { rrouters } from './lib/rrouters'; const app = express(); app.use(express.json()); app.use(cors()); -Object.entries(routes).forEach(([route, router]) => { - app.use(path.join(API_ROOT_PATH, route), router); -}); +rrouters(app, routes, { key: 'api' }); +rrouters(app, routes, { key: 'echo' }); export default app; diff --git a/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts b/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts deleted file mode 100644 index fd320c2d..00000000 --- a/striker-ui-api/src/lib/consts/API_ROOT_PATH.ts +++ /dev/null @@ -1,3 +0,0 @@ -const API_ROOT_PATH = '/api'; - -export default API_ROOT_PATH; diff --git a/striker-ui-api/src/lib/rrouters.ts b/striker-ui-api/src/lib/rrouters.ts new file mode 100644 index 00000000..deb76955 --- /dev/null +++ b/striker-ui-api/src/lib/rrouters.ts @@ -0,0 +1,25 @@ +import { Application, Router } from 'express'; +import path from 'path'; + +import { stdout } from './shell'; + +export const rrouters = < + A extends Application, + M extends MapToRouter, + R extends Router, +>( + app: A, + union: Readonly | R, + { key, route = '/' }: { key?: string; route?: string } = {}, +) => { + if ('route' in union) { + stdout(`Setting up route ${route}`); + app.use(route, union as R); + } else if (key) { + rrouters(app, union[key], { route: path.posix.join(route, key) }); + } else { + Object.entries(union).forEach(([extend, subunion]) => { + rrouters(app, subunion, { route: path.posix.join(route, extend) }); + }); + } +}; diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index ef703777..6566265e 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -1,5 +1,3 @@ -import { Router } from 'express'; - import anvilRouter from './anvil'; import commandRouter from './command'; import echoRouter from './echo'; @@ -14,20 +12,22 @@ import sshKeyRouter from './ssh-key'; import upsRouter from './ups'; import userRouter from './user'; -const routes: Readonly> = { - anvil: anvilRouter, - command: commandRouter, +const routes = { + api: { + anvil: anvilRouter, + command: commandRouter, + fence: fenceRouter, + file: fileRouter, + host: hostRouter, + job: jobRouter, + manifest: manifestRouter, + 'network-interface': networkInterfaceRouter, + server: serverRouter, + 'ssh-key': sshKeyRouter, + ups: upsRouter, + user: userRouter, + }, echo: echoRouter, - fence: fenceRouter, - file: fileRouter, - host: hostRouter, - job: jobRouter, - manifest: manifestRouter, - 'network-interface': networkInterfaceRouter, - server: serverRouter, - 'ssh-key': sshKeyRouter, - ups: upsRouter, - user: userRouter, }; export default routes; diff --git a/striker-ui-api/src/types/MapToRouter.d.ts b/striker-ui-api/src/types/MapToRouter.d.ts new file mode 100644 index 00000000..83137d60 --- /dev/null +++ b/striker-ui-api/src/types/MapToRouter.d.ts @@ -0,0 +1,3 @@ +type MapToRouter = { + [uri: string]: MapToRouter | import('express').Router; +}; From aacbf3003a1ed5a0085994a9bb8abde1b3f61d9b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 11 Apr 2023 13:57:42 -0400 Subject: [PATCH 11/81] chore(striker-ui-api): install passport and local strategy --- striker-ui-api/package-lock.json | 135 ++++++++++++++++++++++++++++++- striker-ui-api/package.json | 6 +- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/package-lock.json b/striker-ui-api/package-lock.json index a5f2b961..11a950af 100644 --- a/striker-ui-api/package-lock.json +++ b/striker-ui-api/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", - "multer": "^1.4.4" + "multer": "^1.4.4", + "passport": "^0.6.0", + "passport-local": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.17.8", @@ -20,6 +22,8 @@ "@types/express": "^4.17.13", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", + "@types/passport": "^1.0.12", + "@types/passport-local": "^1.0.35", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "babel-loader": "^8.2.3", @@ -2445,6 +2449,36 @@ "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", "dev": true }, + "node_modules/@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-local": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz", + "integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -5582,6 +5616,42 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5629,6 +5699,11 @@ "node": ">=8" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -8540,6 +8615,36 @@ "integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==", "dev": true }, + "@types/passport": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz", + "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-local": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz", + "integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -10878,6 +10983,29 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10913,6 +11041,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", diff --git a/striker-ui-api/package.json b/striker-ui-api/package.json index 6cc22f41..a498544f 100644 --- a/striker-ui-api/package.json +++ b/striker-ui-api/package.json @@ -13,7 +13,9 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", - "multer": "^1.4.4" + "multer": "^1.4.4", + "passport": "^0.6.0", + "passport-local": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.17.8", @@ -23,6 +25,8 @@ "@types/express": "^4.17.13", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", + "@types/passport": "^1.0.12", + "@types/passport-local": "^1.0.35", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "babel-loader": "^8.2.3", From fc73f4ec15f7acb789c328b66108b730b1669280 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 11 Apr 2023 19:40:45 -0400 Subject: [PATCH 12/81] build: fully replace apache with nodejs back-end --- anvil.spec.in | 43 ++++++++++++++++++++++++++++-------- units/striker-ui-api.service | 5 ++--- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/anvil.spec.in b/anvil.spec.in index 34cf10ca..c26d1580 100644 --- a/anvil.spec.in +++ b/anvil.spec.in @@ -5,6 +5,7 @@ %define debug_package %{nil} %define anviluser admin %define anvilgroup admin +%define suiapi striker-ui-api Name: anvil Version: @version@ @@ -134,7 +135,6 @@ Requires: firefox Requires: gcc Requires: gdm Requires: gnome-terminal -Requires: httpd Requires: nmap Requires: nodejs Requires: openssh-askpass @@ -243,14 +243,32 @@ setenforce 0 systemctl enable --now chronyd.service systemctl enable --now anvil-daemon.service systemctl enable --now scancore.service -systemctl enable --now striker-ui-api.service -systemctl restart striker-ui-api.service + +%pre striker +getent passwd %{suiapi} >/dev/null \ + || useradd \ + --comment "Striker UI API" \ + --home-dir %{_datadir}/%{suiapi} \ + --shell %{_sbindir}/nologin \ + --user-group \ + %{suiapi} + +if [ $1 -gt 1 ]; then # >1=Upgrade + # Transfer files owned by apache to Striker UI API user. + chown -R --from apache %{suiapi}: /mnt + chown -R --from apache %{suiapi}: %{_localstatedir}/www +fi %post striker ### NOTE: PostgreSQL is initialized and enabled by striker-prep-database later. -echo "Enabling and starting apache." -systemctl enable httpd.service -systemctl start httpd.service + +# Always reload to handle service file changes. +systemctl daemon-reload + +systemctl enable %{suiapi}.service +# Striker UI API needs explicit restart for changes to take effect. +systemctl restart %{suiapi}.service + restorecon -rv /%{_localstatedir}/www if ! $(ls -l /etc/systemd/system/default.target | grep -q graphical); then @@ -329,6 +347,11 @@ touch /etc/anvil/type.dr # sed -i.anvil 's/SELINUX=permissive/SELINUX=enforcing/' /etc/selinux/config # setenforce 1 +%preun striker +if [ $1 == 0 ]; then # 0=Uninstall, 1=First install, >1=Upgrade (version count) + systemctl disable --now %{suiapi}.service +fi + %postun striker ### TODO: Stopping postgres breaks the Anvil! during OS updates. Need to find a ### way to run this only during uninstalls, and not during updates. @@ -340,11 +363,13 @@ touch /etc/anvil/type.dr # firewall-cmd --zone=public --remove-service=postgresql # firewall-cmd --zone=public --remove-service=postgresql --permanent # echo "Disabling and stopping postgresql-9.6." -# systemctl disable httpd.service -# systemctl stop httpd.service # systemctl disable postgresql.service # systemctl stop postgresql.service +if [ $1 == 0 ]; then # 0=Uninstall + systemctl daemon-reload +fi + # Remove the system type file. if [ -e '/etc/anvil/type.striker' ] then @@ -376,7 +401,7 @@ fi %{_sbindir}/* %{_sysconfdir}/anvil/anvil.version %{_datadir}/perl5/* -%{_datadir}/striker-ui-api/* +%{_datadir}/%{suiapi}/* %{_mandir}/* %files striker diff --git a/units/striker-ui-api.service b/units/striker-ui-api.service index 4c7d15f6..aa13ef8a 100644 --- a/units/striker-ui-api.service +++ b/units/striker-ui-api.service @@ -4,9 +4,8 @@ Wants=network.target [Service] Type=simple -# Run as apache to allow the API to access apache-owned locations; i.e., /mnt/shared. -User=apache -Group=apache +User=striker-ui-api +Group=striker-ui-api ExecStart=/usr/bin/node /usr/share/striker-ui-api/index.js ExecStop=/bin/kill -WINCH ${MAINPID} Restart=always From 67b292dd95db405e607c5afff07662dca214665b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 11 Apr 2023 23:52:01 -0400 Subject: [PATCH 13/81] fix(striker-ui-api): change process ownership after port binding --- striker-ui-api/src/index.ts | 23 ++++++++++++++++--- .../src/lib/consts/PROCESS_OWNER.ts | 2 ++ striker-ui-api/src/lib/consts/SERVER_PORT.ts | 4 +--- striker-ui-api/src/lib/consts/index.ts | 2 ++ 4 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 striker-ui-api/src/lib/consts/PROCESS_OWNER.ts create mode 100644 striker-ui-api/src/lib/consts/index.ts diff --git a/striker-ui-api/src/index.ts b/striker-ui-api/src/index.ts index 51c13841..3f49f986 100644 --- a/striker-ui-api/src/index.ts +++ b/striker-ui-api/src/index.ts @@ -1,7 +1,24 @@ +import { getgid, getuid, setgid, setuid } from 'process'; + +import { PGID, PUID, PORT } from './lib/consts'; + import app from './app'; +import { stderr, stdout } from './lib/shell'; + +stdout(`Starting process with ownership ${getuid()}:${getgid()}`); + +app.listen(PORT, () => { + try { + // Group must be set before user to avoid permission error. + setgid(PGID); + setuid(PUID); + + stdout(`Process ownership changed to ${getuid()}:${getgid()}.`); + } catch (error) { + stderr(`Failed to change process ownership; CAUSE: ${error}`); -import SERVER_PORT from './lib/consts/SERVER_PORT'; + process.exit(1); + } -app.listen(SERVER_PORT, () => { - console.log(`Listening on localhost:${SERVER_PORT}.`); + stdout(`Listening on localhost:${PORT}.`); }); diff --git a/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts new file mode 100644 index 00000000..6b118789 --- /dev/null +++ b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts @@ -0,0 +1,2 @@ +export const PUID = process.env.PUID ?? 'striker-ui-api'; +export const PGID = process.env.PGID ?? PUID; diff --git a/striker-ui-api/src/lib/consts/SERVER_PORT.ts b/striker-ui-api/src/lib/consts/SERVER_PORT.ts index db7f5f0e..f668d4c2 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PORT.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PORT.ts @@ -1,3 +1 @@ -const SERVER_PORT = process.env.SERVER_PORT ?? 8080; - -export default SERVER_PORT; +export const PORT = process.env.PORT ?? 8080; diff --git a/striker-ui-api/src/lib/consts/index.ts b/striker-ui-api/src/lib/consts/index.ts new file mode 100644 index 00000000..fa9190c4 --- /dev/null +++ b/striker-ui-api/src/lib/consts/index.ts @@ -0,0 +1,2 @@ +export * from './PROCESS_OWNER'; +export * from './SERVER_PORT'; From 73fa311272a5b2684150e18419654c591eef5bc0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 11 Apr 2023 23:56:27 -0400 Subject: [PATCH 14/81] chore(units): start striker UI API as root and drop privileges later --- units/striker-ui-api.service | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/units/striker-ui-api.service b/units/striker-ui-api.service index aa13ef8a..b568f1f8 100644 --- a/units/striker-ui-api.service +++ b/units/striker-ui-api.service @@ -4,8 +4,9 @@ Wants=network.target [Service] Type=simple -User=striker-ui-api -Group=striker-ui-api +User=root +Group=root +Environment=PORT=80 ExecStart=/usr/bin/node /usr/share/striker-ui-api/index.js ExecStop=/bin/kill -WINCH ${MAINPID} Restart=always From c40b7ff7d4d165a74526f0956d4f70180212abcd Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 11 Apr 2023 23:59:21 -0400 Subject: [PATCH 15/81] fix(striker-ui-api): allow customize handlers per route batch in register routers function --- striker-ui-api/src/lib/rrouters.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/lib/rrouters.ts b/striker-ui-api/src/lib/rrouters.ts index deb76955..619b1bb3 100644 --- a/striker-ui-api/src/lib/rrouters.ts +++ b/striker-ui-api/src/lib/rrouters.ts @@ -10,13 +10,21 @@ export const rrouters = < >( app: A, union: Readonly | R, - { key, route = '/' }: { key?: string; route?: string } = {}, + { + assign = (router) => [router], + key, + route = '/', + }: { + assign?: (router: R) => R[]; + key?: keyof M; + route?: string; + } = {}, ) => { if ('route' in union) { stdout(`Setting up route ${route}`); - app.use(route, union as R); + app.use(route, ...assign(union as R)); } else if (key) { - rrouters(app, union[key], { route: path.posix.join(route, key) }); + rrouters(app, union[key], { route: path.posix.join(route, String(key)) }); } else { Object.entries(union).forEach(([extend, subunion]) => { rrouters(app, subunion, { route: path.posix.join(route, extend) }); From 617998e22b721de24a704371323901924ffe9f9e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 12 Apr 2023 00:05:03 -0400 Subject: [PATCH 16/81] fix(striker-ui-api): add passport login strategy --- striker-ui-api/src/passport.ts | 83 ++++++++++++++++++++++++++++++ striker-ui-api/src/types/User.d.ts | 4 ++ 2 files changed, 87 insertions(+) create mode 100644 striker-ui-api/src/passport.ts create mode 100644 striker-ui-api/src/types/User.d.ts diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts new file mode 100644 index 00000000..09a85481 --- /dev/null +++ b/striker-ui-api/src/passport.ts @@ -0,0 +1,83 @@ +import passport from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; + +import { dbQuery, sub } from './lib/accessModule'; +import { stdout } from './lib/shell'; + +passport.use( + 'login', + new LocalStrategy((username, password, done) => { + stdout(`Attempting passport local strategy [login] for user [${username}]`); + + let rows: [ + userUuid: string, + userName: string, + userPasswordHash: string, + userSalt: string, + userAlgorithm: string, + userHashCount: string, + ][]; + + try { + rows = dbQuery( + `SELECT + user_uuid, + user_name, + user_password_hash, + user_salt, + user_algorithm, + user_hash_count + FROM users + WHERE user_algorithm != 'DELETED' + AND user_name = '${username}' + LIMIT 1;`, + ).stdout; + } catch (queryError) { + return done(queryError); + } + + if (!rows.length) { + return done(null, false); + } + + const { + 0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount], + } = rows; + + let encryptResult: { + user_password_hash: string; + user_salt: string; + user_hash_count: number; + user_algorithm: string; + }; + + try { + encryptResult = sub('encrypt_password', { + subModuleName: 'Account', + subParams: { + algorithm: userAlgorithm, + hash_count: userHashCount, + password, + salt: userSalt, + }, + }).stdout; + } catch (subError) { + return done(subError); + } + + const { user_password_hash: inputPasswordHash } = encryptResult; + + if (inputPasswordHash !== userPasswordHash) { + return done(null, false); + } + + const user: User = { + name: username, + uuid: userUuid, + }; + + return done(null, user); + }), +); + +export default passport; diff --git a/striker-ui-api/src/types/User.d.ts b/striker-ui-api/src/types/User.d.ts new file mode 100644 index 00000000..a0a3a79c --- /dev/null +++ b/striker-ui-api/src/types/User.d.ts @@ -0,0 +1,4 @@ +interface User extends Express.User { + name: string; + uuid: string; +} From 2746e500de89723441a0a8ebd43cd291fcd3af73 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 12 Apr 2023 16:38:12 -0400 Subject: [PATCH 17/81] chore(striker-ui-api): install express-session package --- striker-ui-api/package-lock.json | 137 +++++++++++++++++++++++++++++++ striker-ui-api/package.json | 2 + 2 files changed, 139 insertions(+) diff --git a/striker-ui-api/package-lock.json b/striker-ui-api/package-lock.json index 11a950af..41d491f6 100644 --- a/striker-ui-api/package-lock.json +++ b/striker-ui-api/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", + "express-session": "^1.17.3", "multer": "^1.4.4", "passport": "^0.6.0", "passport-local": "^1.0.0" @@ -20,6 +21,7 @@ "@babel/preset-typescript": "^7.16.7", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-session": "^1.17.7", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", "@types/passport": "^1.0.12", @@ -2416,6 +2418,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -4306,6 +4317,51 @@ "node": ">= 0.10.0" } }, + "node_modules/express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "dependencies": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5519,6 +5575,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5815,6 +5879,14 @@ } ] }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -6521,6 +6593,17 @@ "node": ">=4.2.0" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -8582,6 +8665,15 @@ "@types/range-parser": "*" } }, + "@types/express-session": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz", + "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/json-schema": { "version": "7.0.10", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", @@ -10042,6 +10134,33 @@ } } }, + "express-session": { + "version": "1.17.3", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz", + "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==", + "requires": { + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10913,6 +11032,11 @@ "ee-first": "1.1.1" } }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11113,6 +11237,11 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -11631,6 +11760,14 @@ "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==", "dev": true }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/striker-ui-api/package.json b/striker-ui-api/package.json index a498544f..d40da7ae 100644 --- a/striker-ui-api/package.json +++ b/striker-ui-api/package.json @@ -13,6 +13,7 @@ "dependencies": { "cors": "^2.8.5", "express": "^4.18.2", + "express-session": "^1.17.3", "multer": "^1.4.4", "passport": "^0.6.0", "passport-local": "^1.0.0" @@ -23,6 +24,7 @@ "@babel/preset-typescript": "^7.16.7", "@types/cors": "^2.8.12", "@types/express": "^4.17.13", + "@types/express-session": "^1.17.7", "@types/multer": "^1.4.7", "@types/node": "^17.0.22", "@types/passport": "^1.0.12", From f911a335acbe9b014cbe10a208cf156d80f736e4 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 12 Apr 2023 16:39:53 -0400 Subject: [PATCH 18/81] fix(striker-ui-api): add openssl to shell --- striker-ui-api/src/lib/consts/SERVER_PATHS.ts | 1 + striker-ui-api/src/lib/shell.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index 132d3015..d24140d6 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -11,6 +11,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { bin: { date: {}, mkfifo: {}, + openssl: {}, psql: {}, rm: {}, sed: {}, diff --git a/striker-ui-api/src/lib/shell.ts b/striker-ui-api/src/lib/shell.ts index dcd12f2e..36947da1 100644 --- a/striker-ui-api/src/lib/shell.ts +++ b/striker-ui-api/src/lib/shell.ts @@ -35,6 +35,9 @@ export const date = (...args: string[]) => export const mkfifo = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.mkfifo.self, args); +export const openssl = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.openssl.self, args); + export const rm = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.rm.self, args); From 7c657dfbd589d7fc9ec0c82bc6a00dc3f018e736 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 12 Apr 2023 23:33:03 -0400 Subject: [PATCH 19/81] fix(striker-ui-api): add uuidgen to shell --- striker-ui-api/src/lib/consts/SERVER_PATHS.ts | 1 + striker-ui-api/src/lib/shell.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index d24140d6..f6428321 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -15,6 +15,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { psql: {}, rm: {}, sed: {}, + uuidgen: {}, }, sbin: { 'anvil-access-module': {}, diff --git a/striker-ui-api/src/lib/shell.ts b/striker-ui-api/src/lib/shell.ts index 36947da1..e3e5a73b 100644 --- a/striker-ui-api/src/lib/shell.ts +++ b/striker-ui-api/src/lib/shell.ts @@ -41,6 +41,9 @@ export const openssl = (...args: string[]) => export const rm = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.rm.self, args); +export const uuidgen = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.uuidgen.self, args); + export const stderr = (message: string) => print(message, { stream: 'stderr' }); export const stdout = (message: string) => print(message); From 180aa5480049a895150e8d69d36f389d4c62abde Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 12 Apr 2023 23:35:57 -0400 Subject: [PATCH 20/81] refactor(striker-ui-api): simplify DB refresh timestamp function name --- striker-ui-api/src/lib/accessModule.ts | 2 +- striker-ui-api/src/routes/file.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 6365cd54..cd406ff2 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -214,7 +214,7 @@ export { dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, dbQuery, - dbSubRefreshTimestamp, + dbSubRefreshTimestamp as timestamp, dbWrite, getAnvilData, getLocalHostName, diff --git a/striker-ui-api/src/routes/file.ts b/striker-ui-api/src/routes/file.ts index 1d5e1440..bfcb63ff 100644 --- a/striker-ui-api/src/routes/file.ts +++ b/striker-ui-api/src/routes/file.ts @@ -3,7 +3,7 @@ import express from 'express'; import { dbJobAnvilSyncShared, dbQuery, - dbSubRefreshTimestamp, + timestamp, dbWrite, } from '../lib/accessModule'; import getFile from '../lib/request_handlers/file/getFile'; @@ -26,7 +26,7 @@ router `UPDATE files SET file_type = '${FILE_TYPE_DELETED}', - modified_date = '${dbSubRefreshTimestamp()}' + modified_date = '${timestamp()}' WHERE file_uuid = '${fileUUID}';`, ).stdout; @@ -77,7 +77,7 @@ router UPDATE files SET file_name = '${fileName}', - modified_date = '${dbSubRefreshTimestamp()}' + modified_date = '${timestamp()}' WHERE file_uuid = '${fileUUID}';`; anvilSyncSharedFunctions.push(() => @@ -97,7 +97,7 @@ router UPDATE files SET file_type = '${fileType}', - modified_date = '${dbSubRefreshTimestamp()}' + modified_date = '${timestamp()}' WHERE file_uuid = '${fileUUID}';`; anvilSyncSharedFunctions.push(() => @@ -136,7 +136,7 @@ router UPDATE file_locations SET file_location_active = '${fileLocationActive}', - modified_date = '${dbSubRefreshTimestamp()}' + modified_date = '${timestamp()}' WHERE file_location_uuid = '${fileLocationUUID}';`; const targetHosts = dbQuery( From de2e8e45e120c8c00b9c861a0950bfc685ae02e6 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 13 Apr 2023 17:31:53 -0400 Subject: [PATCH 21/81] fix(striker-ui-api): add function to get session secret --- striker-ui-api/src/index.ts | 4 +- .../src/lib/consts/AN_VARIABLE_NAME_LIST.ts | 1 + .../src/lib/consts/EXIT_CODE_LIST.ts | 2 + striker-ui-api/src/lib/consts/index.ts | 2 + striker-ui-api/src/lib/getSessionSecret.ts | 56 +++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts create mode 100644 striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts create mode 100644 striker-ui-api/src/lib/getSessionSecret.ts diff --git a/striker-ui-api/src/index.ts b/striker-ui-api/src/index.ts index 3f49f986..566f927c 100644 --- a/striker-ui-api/src/index.ts +++ b/striker-ui-api/src/index.ts @@ -1,6 +1,6 @@ import { getgid, getuid, setgid, setuid } from 'process'; -import { PGID, PUID, PORT } from './lib/consts'; +import { PGID, PUID, PORT, ECODE_DROP_PRIVILEGES } from './lib/consts'; import app from './app'; import { stderr, stdout } from './lib/shell'; @@ -17,7 +17,7 @@ app.listen(PORT, () => { } catch (error) { stderr(`Failed to change process ownership; CAUSE: ${error}`); - process.exit(1); + process.exit(ECODE_DROP_PRIVILEGES); } stdout(`Listening on localhost:${PORT}.`); diff --git a/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts b/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts new file mode 100644 index 00000000..28756512 --- /dev/null +++ b/striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts @@ -0,0 +1 @@ +export const VNAME_SESSION_SECRET = 'striker-ui-api::session::secret'; diff --git a/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts b/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts new file mode 100644 index 00000000..33faa997 --- /dev/null +++ b/striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts @@ -0,0 +1,2 @@ +export const ECODE_DROP_PRIVILEGES = 1; +export const ECODE_SESSION_SECRET = 2; diff --git a/striker-ui-api/src/lib/consts/index.ts b/striker-ui-api/src/lib/consts/index.ts index fa9190c4..e5746d3f 100644 --- a/striker-ui-api/src/lib/consts/index.ts +++ b/striker-ui-api/src/lib/consts/index.ts @@ -1,2 +1,4 @@ +export * from './AN_VARIABLE_NAME_LIST'; +export * from './EXIT_CODE_LIST'; export * from './PROCESS_OWNER'; export * from './SERVER_PORT'; diff --git a/striker-ui-api/src/lib/getSessionSecret.ts b/striker-ui-api/src/lib/getSessionSecret.ts new file mode 100644 index 00000000..f12caa8b --- /dev/null +++ b/striker-ui-api/src/lib/getSessionSecret.ts @@ -0,0 +1,56 @@ +import assert from 'assert'; + +import { ECODE_SESSION_SECRET, VNAME_SESSION_SECRET } from './consts'; + +import { dbQuery, variable } from './accessModule'; +import { openssl, stderr, stdout } from './shell'; + +export const getSessionSecret = (): string => { + let sessionSecret: string; + + try { + const rows: [sessionSecret: string][] = dbQuery( + `SELECT variable_value + FROM variables + WHERE variable_name = '${VNAME_SESSION_SECRET}';`, + ).stdout; + + assert(rows.length > 0, 'No existing session secret found.'); + + ({ + 0: [sessionSecret], + } = rows); + + stdout('Found an existing session secret.'); + + return sessionSecret; + } catch (queryError) { + stderr(`Failed to get session secret from database; CAUSE: ${queryError}`); + } + + try { + sessionSecret = openssl('rand', '-base64', '32').trim(); + + stdout('Generated a new session secret.'); + } catch (sysError) { + stderr(`Failed to generate session secret; CAUSE: ${sysError}`); + + process.exit(ECODE_SESSION_SECRET); + } + + try { + const vuuid = variable({ + file: __filename, + variable_name: VNAME_SESSION_SECRET, + variable_value: sessionSecret, + }); + + stdout(`Recorded session secret as variable identified by ${vuuid}.`); + } catch (subError) { + stderr(`Failed to record session secret; CAUSE: ${subError}`); + + process.exit(ECODE_SESSION_SECRET); + } + + return sessionSecret; +}; From eb2c66de9f1c5a9081239596be1f56eba7909e4e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 13 Apr 2023 22:38:40 -0400 Subject: [PATCH 22/81] fix(striker-ui-api): add de/serialize user functions to passport --- striker-ui-api/src/passport.ts | 42 +++++++++++++++++++++++++++++- striker-ui-api/src/types/User.d.ts | 4 +-- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts index 09a85481..ff08b434 100644 --- a/striker-ui-api/src/passport.ts +++ b/striker-ui-api/src/passport.ts @@ -2,12 +2,13 @@ import passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; import { dbQuery, sub } from './lib/accessModule'; +import { sanitize } from './lib/sanitize'; import { stdout } from './lib/shell'; passport.use( 'login', new LocalStrategy((username, password, done) => { - stdout(`Attempting passport local strategy [login] for user [${username}]`); + stdout(`Attempting passport local strategy "login" for user [${username}]`); let rows: [ userUuid: string, @@ -80,4 +81,43 @@ passport.use( }), ); +passport.serializeUser((user, done) => { + const { name, uuid } = user as User; + + stdout(`Serialize user [${name}]`); + + return done(null, uuid); +}); + +passport.deserializeUser((id, done) => { + const uuid = sanitize(id, 'string', { modifierType: 'sql' }); + + stdout(`Deserialize user identified by ${uuid}`); + + let rows: [userName: string][]; + + try { + rows = dbQuery( + `SELECT user_name + FROM users + WHERE user_algorithm != 'DELETED' + AND user_uuid = '${uuid}';`, + ).stdout; + } catch (error) { + return done(error); + } + + if (!rows.length) { + return done(null, false); + } + + const { + 0: [userName], + } = rows; + + const user: User = { name: userName, uuid }; + + return done(null, user); +}); + export default passport; diff --git a/striker-ui-api/src/types/User.d.ts b/striker-ui-api/src/types/User.d.ts index a0a3a79c..9c70c3bc 100644 --- a/striker-ui-api/src/types/User.d.ts +++ b/striker-ui-api/src/types/User.d.ts @@ -1,4 +1,4 @@ -interface User extends Express.User { +type User = Express.User & { name: string; uuid: string; -} +}; From 38181ac301e7284e94d2f5d7e41069707d51b236 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 13 Apr 2023 22:43:02 -0400 Subject: [PATCH 23/81] feat(striker-ui-api): add session handler --- striker-ui-api/src/session.ts | 199 ++++++++++++++++++++++ striker-ui-api/src/types/SessionData.d.ts | 3 + 2 files changed, 202 insertions(+) create mode 100644 striker-ui-api/src/session.ts create mode 100644 striker-ui-api/src/types/SessionData.d.ts diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts new file mode 100644 index 00000000..1a40fa1c --- /dev/null +++ b/striker-ui-api/src/session.ts @@ -0,0 +1,199 @@ +import assert from 'assert'; +import session, { + SessionData as BaseSessionData, + Store as BaseStore, +} from 'express-session'; + +import { + dbQuery, + dbWrite, + getLocalHostUUID, + timestamp, +} from './lib/accessModule'; +import { getSessionSecret } from './lib/getSessionSecret'; +import { stdout, stdoutVar, uuidgen } from './lib/shell'; + +const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 1000 * 60 * 60; + +export class SessionStore extends BaseStore { + constructor(options = {}) { + super(options); + } + + public destroy( + sid: string, + done?: ((err?: unknown) => void) | undefined, + ): void { + stdout(`Destroy session ${sid}`); + + try { + const { write_code: wcode }: { write_code: number } = dbWrite( + `DELETE FROM sessions WHERE session_uuid = '${sid}';`, + ).stdout; + + assert(wcode === 0, `Delete session ${sid} failed with code ${wcode}`); + } catch (writeError) { + return done?.call(null, writeError); + } + + return done?.call(null); + } + + public get( + sid: string, + done: (err: unknown, session?: BaseSessionData | null | undefined) => void, + ): void { + stdout(`Get session ${sid}`); + + let rows: [ + sessionUuid: string, + userUuid: string, + sessionModifiedDate: string, + ][]; + + try { + rows = dbQuery( + `SELECT + s.session_uuid, + u.user_uuid, + s.modified_date + FROM sessions AS s + JOIN users AS u + ON s.session_user_uuid = u.user_uuid + WHERE s.session_uuid = '${sid}';`, + ).stdout; + } catch (queryError) { + return done(queryError); + } + + if (!rows.length) { + return done(null); + } + + const { + 0: [, userUuid, sessionModifiedDate], + } = rows; + + const cookieMaxAge = + SessionStore.calculateCookieMaxAge(sessionModifiedDate); + + const data: SessionData = { + cookie: { + maxAge: cookieMaxAge, + originalMaxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + }, + passport: { + user: userUuid, + }, + }; + + return done(null, data); + } + + public set( + sid: string, + session: BaseSessionData, + done?: ((err?: unknown) => void) | undefined, + ): void { + stdout(`Set session ${sid}; session=${JSON.stringify(session, null, 2)}`); + + const { + passport: { user: userUuid }, + } = session as SessionData; + + try { + const localHostUuid = getLocalHostUUID(); + const modifiedDate = timestamp(); + + const { write_code: wcode }: { write_code: number } = dbWrite( + `INSERT INTO + sessions ( + session_uuid, + session_host_uuid, + session_user_uuid, + session_salt, + modified_date + ) + VALUES + ( + '${sid}', + '${localHostUuid}', + '${userUuid}', + '', + '${modifiedDate}' + ) + ON CONFLICT (session_uuid) + DO UPDATE SET session_host_uuid = '${localHostUuid}', + modified_date = '${modifiedDate}';`, + ).stdout; + + assert( + wcode === 0, + `Insert or update session ${sid} failed with code ${wcode}`, + ); + } catch (error) { + return done?.call(null, error); + } + + return done?.call(null); + } + + public touch( + sid: string, + session: BaseSessionData, + done?: ((err?: unknown) => void) | undefined, + ): void { + stdout( + `Update modified date in session ${sid}; session=${JSON.stringify( + session, + null, + 2, + )}`, + ); + + try { + const { write_code: wcode }: { write_code: number } = dbWrite( + `UPDATE sessions SET modified_date = '${timestamp()}' WHERE session_uuid = '${sid}';`, + ).stdout; + + assert( + wcode === 0, + `Update modified date for session ${sid} failed with code ${wcode}`, + ); + } catch (writeError) { + return done?.call(null, writeError); + } + + return done?.call(null); + } + + public static calculateCookieMaxAge( + sessionModifiedDate: string, + cookieOriginalMaxAge: number = DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + ) { + const sessionModifiedEpoch = Date.parse(sessionModifiedDate); + const sessionDeadlineEpoch = sessionModifiedEpoch + cookieOriginalMaxAge; + const cookieMaxAge = sessionDeadlineEpoch - Date.now(); + + stdoutVar({ sessionModifiedDate, sessionDeadlineEpoch, cookieMaxAge }); + + return cookieMaxAge; + } +} + +const sessionHandler = session({ + cookie: { maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE }, + genid: ({ path }) => { + const sid = uuidgen('--random').trim(); + + stdout(`Generated session identifier ${sid}; request.path=${path}`); + + return sid; + }, + resave: false, + saveUninitialized: false, + secret: getSessionSecret(), + store: new SessionStore(), +}); + +export default sessionHandler; diff --git a/striker-ui-api/src/types/SessionData.d.ts b/striker-ui-api/src/types/SessionData.d.ts new file mode 100644 index 00000000..e9a56b71 --- /dev/null +++ b/striker-ui-api/src/types/SessionData.d.ts @@ -0,0 +1,3 @@ +type SessionData = import('express-session').SessionData & { + passport: { user: string }; +}; From 1c8050caa948be32821f15f08580929ab1ca5f95 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 13 Apr 2023 22:44:21 -0400 Subject: [PATCH 24/81] feat(striker-ui-api): apply session handler and add /auth/login --- striker-ui-api/src/app.ts | 15 ++- .../src/lib/request_handlers/auth/index.ts | 1 + .../src/lib/request_handlers/auth/login.ts | 101 +----------------- striker-ui-api/src/routes/auth.ts | 10 ++ striker-ui-api/src/routes/index.ts | 2 + 5 files changed, 29 insertions(+), 100 deletions(-) create mode 100644 striker-ui-api/src/lib/request_handlers/auth/index.ts create mode 100644 striker-ui-api/src/routes/auth.ts diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 617bb09e..8ae7df14 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,15 +1,26 @@ import cors from 'cors'; -import express from 'express'; +import express, { json } from 'express'; +import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; +import sessionHandler from './session'; const app = express(); -app.use(express.json()); +app.use(json()); + app.use(cors()); +// Add session handler to the chain **after** adding other handlers that do +// not depend on session(s). +app.use(sessionHandler); + +app.use(passport.initialize()); +app.use(passport.authenticate('session')); + rrouters(app, routes, { key: 'api' }); +rrouters(app, routes, { key: 'auth' }); rrouters(app, routes, { key: 'echo' }); export default app; diff --git a/striker-ui-api/src/lib/request_handlers/auth/index.ts b/striker-ui-api/src/lib/request_handlers/auth/index.ts new file mode 100644 index 00000000..6cc1e6e2 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/index.ts @@ -0,0 +1 @@ +export * from './login'; diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts index 040e7052..1c27f6fe 100644 --- a/striker-ui-api/src/lib/request_handlers/auth/login.ts +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -1,108 +1,13 @@ -import assert from 'assert'; import { RequestHandler } from 'express'; -import { REP_PEACEFUL_STRING } from '../../consts/REG_EXP_PATTERNS'; - -import { dbQuery, sub } from '../../accessModule'; -import { sanitize } from '../../sanitize'; -import { stderr } from '../../shell'; +import { stdout } from '../../shell'; export const login: RequestHandler = ( request, response, ) => { - const { - body: { password: rawPassword, username: rawUsername }, - } = request; - - const password = sanitize(rawPassword, 'string'); - const username = sanitize(rawUsername, 'string', { modifierType: 'sql' }); - - try { - assert( - REP_PEACEFUL_STRING.test(username), - `Username must be a peaceful string; got [${username}]`, - ); - - assert( - REP_PEACEFUL_STRING.test(password), - `Password must be a peaceful string; got [${password}]`, - ); - } catch (assertError) { - stderr( - `Assertion failed when attempting to authenticate; CAUSE: ${assertError}`, - ); - - response.status(400).send(); - - return; - } - - let rows: [ - userUuid: string, - userPasswordHash: string, - userSalt: string, - userAlgorithm: string, - userHashCount: string, - ][]; - - try { - rows = dbQuery(` - SELECT - user_uuid, - user_password_hash, - user_salt, - user_algorithm, - user_hash_count - FROM users - WHERE user_algorithm != 'DELETED' - AND user_name = '${username}'`).stdout; - } catch (queryError) { - stderr(`Failed to get user ${username}; CAUSE: ${queryError}`); - response.status(500).send(); - return; - } - - if (rows.length === 0) { - stderr(`No entry for user ${username} found`); - response.status(404).send(); - return; - } - - const { - 0: { 1: userPasswordHash, 2: userSalt, 3: userAlgorithm, 4: userHashCount }, - } = rows; - - let encryptResult: { - user_password_hash: string; - user_salt: string; - user_hash_count: number; - user_algorithm: string; - }; - - try { - encryptResult = sub('encrypt_password', { - subModuleName: 'Account', - subParams: { - algorithm: userAlgorithm, - hash_count: userHashCount, - password, - salt: userSalt, - }, - }).stdout; - } catch (subError) { - stderr(`Failed to login with username ${username}; CAUSE: ${subError}`); - response.status(500).send(); - return; - } - - const { user_password_hash: inputPasswordHash } = encryptResult; - - if (inputPasswordHash !== userPasswordHash) { - stderr(`Input and recorded password mismatched.`); - response.status(400).send(); - return; - } + stdout(`session=${JSON.stringify(request.session, null, 2)}`); + stdout(`user=${JSON.stringify(request.user, null, 2)}`); response.status(200).send(); }; diff --git a/striker-ui-api/src/routes/auth.ts b/striker-ui-api/src/routes/auth.ts new file mode 100644 index 00000000..0a2475e1 --- /dev/null +++ b/striker-ui-api/src/routes/auth.ts @@ -0,0 +1,10 @@ +import express from 'express'; + +import { login } from '../lib/request_handlers/auth'; +import passport from '../passport'; + +const router = express.Router(); + +router.post('/login', passport.authenticate('login'), login); + +export default router; diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index 6566265e..29b3719a 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -1,4 +1,5 @@ import anvilRouter from './anvil'; +import authRouter from './auth'; import commandRouter from './command'; import echoRouter from './echo'; import fenceRouter from './fence'; @@ -27,6 +28,7 @@ const routes = { ups: upsRouter, user: userRouter, }, + auth: authRouter, echo: echoRouter, }; From 0fa614ca07e7a0b7e18b1da5eda1accc7102977d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 14 Apr 2023 20:09:57 -0400 Subject: [PATCH 25/81] fix(striker-ui-api): output user name when login succeed --- striker-ui-api/src/lib/request_handlers/auth/login.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts index 1c27f6fe..87ae4404 100644 --- a/striker-ui-api/src/lib/request_handlers/auth/login.ts +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -6,8 +6,13 @@ export const login: RequestHandler = ( request, response, ) => { - stdout(`session=${JSON.stringify(request.session, null, 2)}`); - stdout(`user=${JSON.stringify(request.user, null, 2)}`); + const { user } = request; + + if (user) { + const { name: userName } = user as User; + + stdout(`Successfully authenticated user [${userName}]`); + } response.status(200).send(); }; From 448a9563137bd912b02b32aedd90cb91e17da0ab Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 17 Apr 2023 15:29:11 -0400 Subject: [PATCH 26/81] refactor(striker-ui-api): merge Express.User and SessionData declarations --- .../src/lib/request_handlers/auth/login.ts | 2 +- striker-ui-api/src/passport.ts | 7 ++++--- striker-ui-api/src/session.ts | 14 +++++++------- striker-ui-api/src/types/SessionData.d.ts | 15 ++++++++++++--- striker-ui-api/src/types/User.d.ts | 18 ++++++++++++++---- 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts index 87ae4404..ee4c99fd 100644 --- a/striker-ui-api/src/lib/request_handlers/auth/login.ts +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -9,7 +9,7 @@ export const login: RequestHandler = ( const { user } = request; if (user) { - const { name: userName } = user as User; + const { name: userName } = user; stdout(`Successfully authenticated user [${userName}]`); } diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts index ff08b434..b9c3fe1e 100644 --- a/striker-ui-api/src/passport.ts +++ b/striker-ui-api/src/passport.ts @@ -1,3 +1,4 @@ +import { Express } from 'express'; import passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; @@ -72,7 +73,7 @@ passport.use( return done(null, false); } - const user: User = { + const user: Express.User = { name: username, uuid: userUuid, }; @@ -82,7 +83,7 @@ passport.use( ); passport.serializeUser((user, done) => { - const { name, uuid } = user as User; + const { name, uuid } = user; stdout(`Serialize user [${name}]`); @@ -115,7 +116,7 @@ passport.deserializeUser((id, done) => { 0: [userName], } = rows; - const user: User = { name: userName, uuid }; + const user: Express.User = { name: userName, uuid }; return done(null, user); }); diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index 1a40fa1c..cdf99a10 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import session, { - SessionData as BaseSessionData, - Store as BaseStore, + SessionData, + Store as BaseSessionStore, } from 'express-session'; import { @@ -15,7 +15,7 @@ import { stdout, stdoutVar, uuidgen } from './lib/shell'; const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 1000 * 60 * 60; -export class SessionStore extends BaseStore { +export class SessionStore extends BaseSessionStore { constructor(options = {}) { super(options); } @@ -41,7 +41,7 @@ export class SessionStore extends BaseStore { public get( sid: string, - done: (err: unknown, session?: BaseSessionData | null | undefined) => void, + done: (err: unknown, session?: SessionData | null | undefined) => void, ): void { stdout(`Get session ${sid}`); @@ -92,14 +92,14 @@ export class SessionStore extends BaseStore { public set( sid: string, - session: BaseSessionData, + session: SessionData, done?: ((err?: unknown) => void) | undefined, ): void { stdout(`Set session ${sid}; session=${JSON.stringify(session, null, 2)}`); const { passport: { user: userUuid }, - } = session as SessionData; + } = session; try { const localHostUuid = getLocalHostUUID(); @@ -140,7 +140,7 @@ export class SessionStore extends BaseStore { public touch( sid: string, - session: BaseSessionData, + session: SessionData, done?: ((err?: unknown) => void) | undefined, ): void { stdout( diff --git a/striker-ui-api/src/types/SessionData.d.ts b/striker-ui-api/src/types/SessionData.d.ts index e9a56b71..e186fad1 100644 --- a/striker-ui-api/src/types/SessionData.d.ts +++ b/striker-ui-api/src/types/SessionData.d.ts @@ -1,3 +1,12 @@ -type SessionData = import('express-session').SessionData & { - passport: { user: string }; -}; +declare module 'express-session' { + /** + * Extended with passport property. + */ + interface SessionData { + passport: { user: string }; + returnTo?: string; + } +} + +// Required to avoid overwritting the original express-session module. +export {}; diff --git a/striker-ui-api/src/types/User.d.ts b/striker-ui-api/src/types/User.d.ts index 9c70c3bc..871374ef 100644 --- a/striker-ui-api/src/types/User.d.ts +++ b/striker-ui-api/src/types/User.d.ts @@ -1,4 +1,14 @@ -type User = Express.User & { - name: string; - uuid: string; -}; +declare global { + namespace Express { + /** + * Extended Express.User object used by express-session and passport to + * identify which user owns a session. + */ + interface User { + name: string; + uuid: string; + } + } +} + +export {}; From b0e7859d2dfd0226449d790ab5e0391e2c9b12c3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 17 Apr 2023 18:18:01 -0400 Subject: [PATCH 27/81] fix(striker-ui-api): simplify local host name/uuid getter --- striker-ui-api/src/lib/accessModule.ts | 28 +++++++++++-------- striker-ui-api/src/lib/consts/SERVER_PATHS.ts | 4 +++ striker-ui-api/src/lib/consts/index.ts | 4 +++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index cd406ff2..3a252bc3 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -1,6 +1,7 @@ import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { readFileSync } from 'fs'; -import SERVER_PATHS from './consts/SERVER_PATHS'; +import { SERVER_PATHS } from './consts'; import { stderr as sherr, stdout as shout } from './shell'; @@ -8,15 +9,18 @@ const formatQuery = (query: string) => query.replace(/\s+/g, ' '); const execAnvilAccessModule = ( args: string[], - options: SpawnSyncOptions = { - encoding: 'utf-8', - timeout: 10000, - }, + options: SpawnSyncOptions = {}, ) => { + const { + encoding = 'utf-8', + timeout = 10000, + ...restSpawnSyncOptions + } = options; + const { error, stdout, stderr } = spawnSync( SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, - options, + { encoding, timeout, ...restSpawnSyncOptions }, ); if (error) { @@ -152,9 +156,9 @@ const getLocalHostName = () => { let result: string; try { - result = execModuleSubroutine('host_name', { - subModuleName: 'Get', - }).stdout; + result = readFileSync(SERVER_PATHS.etc.hostname.self, { + encoding: 'utf-8', + }).trim(); } catch (subError) { throw new Error(`Failed to get local host name; CAUSE: ${subError}`); } @@ -168,9 +172,9 @@ const getLocalHostUUID = () => { let result: string; try { - result = execModuleSubroutine('host_uuid', { - subModuleName: 'Get', - }).stdout; + result = readFileSync(SERVER_PATHS.etc.anvil['host.uuid'].self, { + encoding: 'utf-8', + }).trim(); } catch (subError) { throw new Error(`Failed to get local host UUID; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index f6428321..2d089eef 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -1,6 +1,10 @@ import path from 'path'; const EMPTY_SERVER_PATHS: ServerPath = { + etc: { + anvil: { 'host.uuid': {} }, + hostname: {}, + }, mnt: { shared: { incoming: {}, diff --git a/striker-ui-api/src/lib/consts/index.ts b/striker-ui-api/src/lib/consts/index.ts index e5746d3f..ee112bfe 100644 --- a/striker-ui-api/src/lib/consts/index.ts +++ b/striker-ui-api/src/lib/consts/index.ts @@ -1,3 +1,7 @@ +import SERVER_PATHS from './SERVER_PATHS'; + +export { SERVER_PATHS }; + export * from './AN_VARIABLE_NAME_LIST'; export * from './EXIT_CODE_LIST'; export * from './PROCESS_OWNER'; From 410a5de48d9f790929b55cd22b119def49e86055 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 17 Apr 2023 18:46:46 -0400 Subject: [PATCH 28/81] fix(striker-ui-api): generate db timestamp from date --- striker-ui-api/src/lib/accessModule.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 3a252bc3..f6d5c705 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -3,7 +3,7 @@ import { readFileSync } from 'fs'; import { SERVER_PATHS } from './consts'; -import { stderr as sherr, stdout as shout } from './shell'; +import { date, stderr as sherr, stdout as shout } from './shell'; const formatQuery = (query: string) => query.replace(/\s+/g, ' '); @@ -129,8 +129,19 @@ const dbQuery = (query: string, options?: SpawnSyncOptions) => { return execAnvilAccessModule(['--query', query], options); }; -const dbSubRefreshTimestamp = () => - execModuleSubroutine('refresh_timestamp').stdout; +const dbSubRefreshTimestamp = () => { + let result: string; + + try { + result = date('--rfc-3339', 'ns').trim(); + } catch (shError) { + throw new Error( + `Failed to get timestamp for database use; CAUSE: ${shError}`, + ); + } + + return result; +}; const dbWrite = (query: string, options?: SpawnSyncOptions) => { shout(formatQuery(query)); From 17583ad4b9846dd8b8f1c332662a019584e657e3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 00:15:58 -0400 Subject: [PATCH 29/81] fix(striker-ui-api): add formatSql() --- striker-ui-api/src/lib/formatSql.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 striker-ui-api/src/lib/formatSql.ts diff --git a/striker-ui-api/src/lib/formatSql.ts b/striker-ui-api/src/lib/formatSql.ts new file mode 100644 index 00000000..b4a7acb4 --- /dev/null +++ b/striker-ui-api/src/lib/formatSql.ts @@ -0,0 +1 @@ +export const formatSql = (script: string) => script.replace(/\s+/g, ' '); From a1c7be94d6d98be56a8f7ec5785a5ba05f31f21e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 00:17:02 -0400 Subject: [PATCH 30/81] fix(striker-ui-api): add isObject() --- striker-ui-api/src/lib/isObject.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 striker-ui-api/src/lib/isObject.ts diff --git a/striker-ui-api/src/lib/isObject.ts b/striker-ui-api/src/lib/isObject.ts new file mode 100644 index 00000000..caab9fe2 --- /dev/null +++ b/striker-ui-api/src/lib/isObject.ts @@ -0,0 +1,10 @@ +export const isObject = (value: unknown) => { + const result: { is: boolean; obj: object } = { is: false, obj: {} }; + + if (typeof value === 'object' && value !== null) { + result.is = true; + result.obj = value; + } + + return result; +}; From 9b750c57f9afb651bdfefff9710785b63e7e7b2b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 00:18:43 -0400 Subject: [PATCH 31/81] fix(striker-ui-api): add async database write function --- striker-ui-api/src/lib/accessModule.ts | 73 +++++++++++++++++++--- striker-ui-api/src/types/AccessModule.d.ts | 9 +++ 2 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 striker-ui-api/src/types/AccessModule.d.ts diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index f6d5c705..92335c57 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -1,11 +1,58 @@ -import { spawnSync, SpawnSyncOptions } from 'child_process'; +import { spawn, spawnSync, SpawnSyncOptions } from 'child_process'; import { readFileSync } from 'fs'; import { SERVER_PATHS } from './consts'; +import { formatSql } from './formatSql'; import { date, stderr as sherr, stdout as shout } from './shell'; -const formatQuery = (query: string) => query.replace(/\s+/g, ' '); +const asyncAnvilAccessModule = ( + args: string[], + { + onClose, + onError, + stdio = 'pipe', + timeout = 60000, + ...restSpawnOptions + }: AsyncAnvilAccessModuleOptions = {}, +) => { + const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { + stdio, + timeout, + ...restSpawnOptions, + }); + + let stderr = ''; + let stdout = ''; + + ps.once('close', (ecode, signal) => { + let output; + + try { + output = JSON.parse(stdout); + } catch (stdoutParseError) { + output = stdout; + + sherr( + `Failed to parse async anvil-access-module stdout; CAUSE: ${stdoutParseError}`, + ); + } + + onClose?.call(null, { ecode, signal, stderr, stdout: output }); + }); + + ps.once('error', (...args) => { + onError?.call(null, ...args); + }); + + ps.stderr?.setEncoding('utf-8').on('data', (data) => { + stderr += data; + }); + + ps.stdout?.setEncoding('utf-8').on('data', (data) => { + stdout += data; + }); +}; const execAnvilAccessModule = ( args: string[], @@ -17,7 +64,7 @@ const execAnvilAccessModule = ( ...restSpawnSyncOptions } = options; - const { error, stdout, stderr } = spawnSync( + const { error, stderr, stdout } = spawnSync( SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { encoding, timeout, ...restSpawnSyncOptions }, @@ -39,7 +86,7 @@ const execAnvilAccessModule = ( output = stdout; sherr( - `Failed to parse anvil-access-module output [${output}]; CAUSE: [${stdoutParseError}]`, + `Failed to parse anvil-access-module stdout; CAUSE: ${stdoutParseError}`, ); } @@ -124,7 +171,7 @@ const dbJobAnvilSyncShared = ( }; const dbQuery = (query: string, options?: SpawnSyncOptions) => { - shout(formatQuery(query)); + shout(formatSql(query)); return execAnvilAccessModule(['--query', query], options); }; @@ -143,10 +190,19 @@ const dbSubRefreshTimestamp = () => { return result; }; -const dbWrite = (query: string, options?: SpawnSyncOptions) => { - shout(formatQuery(query)); +const awrite = (script: string, options?: AsyncAnvilAccessModuleOptions) => { + shout(formatSql(script)); + + return asyncAnvilAccessModule( + ['--query', script, '--mode', 'write'], + options, + ); +}; + +const dbWrite = (script: string, options?: SpawnSyncOptions) => { + shout(formatSql(script)); - return execAnvilAccessModule(['--query', query, '--mode', 'write'], options); + return execAnvilAccessModule(['--query', script, '--mode', 'write'], options); }; const getAnvilData = ( @@ -225,6 +281,7 @@ const getPeerData: GetPeerDataFunction = ( }; export { + awrite, dbInsertOrUpdateJob as job, dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, diff --git a/striker-ui-api/src/types/AccessModule.d.ts b/striker-ui-api/src/types/AccessModule.d.ts new file mode 100644 index 00000000..d5b00ed7 --- /dev/null +++ b/striker-ui-api/src/types/AccessModule.d.ts @@ -0,0 +1,9 @@ +type AsyncAnvilAccessModuleOptions = import('child_process').SpawnOptions & { + onClose?: (args: { + ecode: number | null; + signal: NodeJS.Signals | null; + stderr: string; + stdout: unknown; + }) => void; + onError?: (err: Error) => void; +}; From ffe3c0853f2a85723ed9544b549a454ebcb7bc02 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 00:21:24 -0400 Subject: [PATCH 32/81] fix(striker-ui-api): make all writes in session store async to reduce response time --- striker-ui-api/src/session.ts | 101 +++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 32 deletions(-) diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index cdf99a10..ca22978d 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -1,20 +1,30 @@ -import assert from 'assert'; import session, { SessionData, Store as BaseSessionStore, } from 'express-session'; import { + awrite, dbQuery, - dbWrite, getLocalHostUUID, timestamp, } from './lib/accessModule'; import { getSessionSecret } from './lib/getSessionSecret'; -import { stdout, stdoutVar, uuidgen } from './lib/shell'; +import { isObject } from './lib/isObject'; +import { stderr, stdout, stdoutVar, uuidgen } from './lib/shell'; const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 1000 * 60 * 60; +const getWriteCode = (obj: object) => { + let result: number | undefined; + + if ('write_code' in obj) { + ({ write_code: result } = obj as { write_code: number }); + } + + return result; +}; + export class SessionStore extends BaseSessionStore { constructor(options = {}) { super(options); @@ -27,13 +37,24 @@ export class SessionStore extends BaseSessionStore { stdout(`Destroy session ${sid}`); try { - const { write_code: wcode }: { write_code: number } = dbWrite( - `DELETE FROM sessions WHERE session_uuid = '${sid}';`, - ).stdout; - - assert(wcode === 0, `Delete session ${sid} failed with code ${wcode}`); - } catch (writeError) { - return done?.call(null, writeError); + awrite(`DELETE FROM sessions WHERE session_uuid = '${sid}';`, { + onClose({ stdout: s1 }) { + const wcode = getWriteCode(isObject(s1).obj); + + if (wcode !== 0) { + stderr( + `SQL script failed during destroy session ${sid}; code: ${wcode}`, + ); + } + }, + onError(error) { + stderr( + `Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`, + ); + }, + }); + } catch (error) { + return done?.call(null, error); } return done?.call(null); @@ -95,7 +116,7 @@ export class SessionStore extends BaseSessionStore { session: SessionData, done?: ((err?: unknown) => void) | undefined, ): void { - stdout(`Set session ${sid}; session=${JSON.stringify(session, null, 2)}`); + stdout(`Set session ${sid}`); const { passport: { user: userUuid }, @@ -105,7 +126,7 @@ export class SessionStore extends BaseSessionStore { const localHostUuid = getLocalHostUUID(); const modifiedDate = timestamp(); - const { write_code: wcode }: { write_code: number } = dbWrite( + awrite( `INSERT INTO sessions ( session_uuid, @@ -125,11 +146,22 @@ export class SessionStore extends BaseSessionStore { ON CONFLICT (session_uuid) DO UPDATE SET session_host_uuid = '${localHostUuid}', modified_date = '${modifiedDate}';`, - ).stdout; - - assert( - wcode === 0, - `Insert or update session ${sid} failed with code ${wcode}`, + { + onClose: ({ stdout: s1 }) => { + const wcode = getWriteCode(isObject(s1).obj); + + if (wcode !== 0) { + stderr( + `SQL script failed during set session ${sid}; code: ${wcode}`, + ); + } + }, + onError: (error) => { + stderr( + `Failed to complete DB write in set session ${sid}; CAUSE: ${error}`, + ); + }, + }, ); } catch (error) { return done?.call(null, error); @@ -143,25 +175,30 @@ export class SessionStore extends BaseSessionStore { session: SessionData, done?: ((err?: unknown) => void) | undefined, ): void { - stdout( - `Update modified date in session ${sid}; session=${JSON.stringify( - session, - null, - 2, - )}`, - ); + stdout(`Touch session ${sid}`); try { - const { write_code: wcode }: { write_code: number } = dbWrite( + awrite( `UPDATE sessions SET modified_date = '${timestamp()}' WHERE session_uuid = '${sid}';`, - ).stdout; - - assert( - wcode === 0, - `Update modified date for session ${sid} failed with code ${wcode}`, + { + onClose: ({ stdout: s1 }) => { + const wcode = getWriteCode(isObject(s1).obj); + + if (wcode !== 0) { + stderr( + `SQL script failed during touch session ${sid}; code: ${wcode}`, + ); + } + }, + onError: (error) => { + stderr( + `Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`, + ); + }, + }, ); - } catch (writeError) { - return done?.call(null, writeError); + } catch (error) { + return done?.call(null, error); } return done?.call(null); From a4524b6d2758cc952778e40029f7ceb27c839b8c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 16:47:26 -0400 Subject: [PATCH 33/81] fix(striker-ui-api): add /auth/logout --- .../src/lib/request_handlers/auth/index.ts | 1 + .../src/lib/request_handlers/auth/logout.ts | 17 +++++++++++++++++ striker-ui-api/src/routes/auth.ts | 7 +++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 striker-ui-api/src/lib/request_handlers/auth/logout.ts diff --git a/striker-ui-api/src/lib/request_handlers/auth/index.ts b/striker-ui-api/src/lib/request_handlers/auth/index.ts index 6cc1e6e2..8c008ef4 100644 --- a/striker-ui-api/src/lib/request_handlers/auth/index.ts +++ b/striker-ui-api/src/lib/request_handlers/auth/index.ts @@ -1 +1,2 @@ export * from './login'; +export * from './logout'; diff --git a/striker-ui-api/src/lib/request_handlers/auth/logout.ts b/striker-ui-api/src/lib/request_handlers/auth/logout.ts new file mode 100644 index 00000000..c6b28e24 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/auth/logout.ts @@ -0,0 +1,17 @@ +import { RequestHandler } from 'express'; + +import { stdout } from '../../shell'; + +export const logout: RequestHandler = (request, response) => { + request.session.destroy((error) => { + let scode = 204; + + if (error) { + scode = 500; + + stdout(`Failed to destroy session upon logout; CAUSE: ${error}`); + } + + response.status(scode).send(); + }); +}; diff --git a/striker-ui-api/src/routes/auth.ts b/striker-ui-api/src/routes/auth.ts index 0a2475e1..2be436ef 100644 --- a/striker-ui-api/src/routes/auth.ts +++ b/striker-ui-api/src/routes/auth.ts @@ -1,10 +1,13 @@ import express from 'express'; -import { login } from '../lib/request_handlers/auth'; +import { assertAuthentication } from '../lib/assertAuthentication'; +import { login, logout } from '../lib/request_handlers/auth'; import passport from '../passport'; const router = express.Router(); -router.post('/login', passport.authenticate('login'), login); +router + .post('/login', passport.authenticate('login'), login) + .put('/logout', assertAuthentication(), logout); export default router; From 4a1cc577b54d80f1ccb958b971ff3f5707e81448 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 17:03:30 -0400 Subject: [PATCH 34/81] fix(striker-ui-api): correct extend handler list in register routers --- striker-ui-api/src/lib/rrouters.ts | 25 ++++++++++++++++------- striker-ui-api/src/types/MapToRouter.d.ts | 4 ++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/striker-ui-api/src/lib/rrouters.ts b/striker-ui-api/src/lib/rrouters.ts index 619b1bb3..0ecaa262 100644 --- a/striker-ui-api/src/lib/rrouters.ts +++ b/striker-ui-api/src/lib/rrouters.ts @@ -1,12 +1,13 @@ -import { Application, Router } from 'express'; +import { Application, Handler, Router } from 'express'; import path from 'path'; import { stdout } from './shell'; export const rrouters = < A extends Application, - M extends MapToRouter, + M extends MapToRouter, R extends Router, + H extends Handler, >( app: A, union: Readonly | R, @@ -15,19 +16,29 @@ export const rrouters = < key, route = '/', }: { - assign?: (router: R) => R[]; + assign?: (router: R) => Array; key?: keyof M; route?: string; } = {}, ) => { if ('route' in union) { - stdout(`Setting up route ${route}`); - app.use(route, ...assign(union as R)); + const handlers = assign(union as R); + const { length: hcount } = handlers; + + stdout(`Set up route ${route} with ${hcount} handler(s)`); + + app.use(route, ...handlers); } else if (key) { - rrouters(app, union[key], { route: path.posix.join(route, String(key)) }); + rrouters(app, union[key], { + assign, + route: path.posix.join(route, String(key)), + }); } else { Object.entries(union).forEach(([extend, subunion]) => { - rrouters(app, subunion, { route: path.posix.join(route, extend) }); + rrouters(app, subunion, { + assign, + route: path.posix.join(route, extend), + }); }); } }; diff --git a/striker-ui-api/src/types/MapToRouter.d.ts b/striker-ui-api/src/types/MapToRouter.d.ts index 83137d60..5380d644 100644 --- a/striker-ui-api/src/types/MapToRouter.d.ts +++ b/striker-ui-api/src/types/MapToRouter.d.ts @@ -1,3 +1,3 @@ -type MapToRouter = { - [uri: string]: MapToRouter | import('express').Router; +type MapToRouter = { + [uri: string]: MapToRouter | R; }; From 193727b93f3d30f4ac82df3613f4a7d9ed01c15e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 17:08:23 -0400 Subject: [PATCH 35/81] fix(striker-ui-api): add assert authentication --- striker-ui-api/src/app.ts | 8 +++- .../src/lib/assertAuthentication.ts | 37 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 striker-ui-api/src/lib/assertAuthentication.ts diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 8ae7df14..feea2742 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,6 +1,7 @@ import cors from 'cors'; import express, { json } from 'express'; +import { assertAuthentication } from './lib/assertAuthentication'; import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; @@ -19,7 +20,12 @@ app.use(sessionHandler); app.use(passport.initialize()); app.use(passport.authenticate('session')); -rrouters(app, routes, { key: 'api' }); +const authenticationHandler = assertAuthentication(); + +rrouters(app, routes, { + assign: (router) => [authenticationHandler, router], + key: 'api', +}); rrouters(app, routes, { key: 'auth' }); rrouters(app, routes, { key: 'echo' }); diff --git a/striker-ui-api/src/lib/assertAuthentication.ts b/striker-ui-api/src/lib/assertAuthentication.ts new file mode 100644 index 00000000..a49d7889 --- /dev/null +++ b/striker-ui-api/src/lib/assertAuthentication.ts @@ -0,0 +1,37 @@ +import { Handler, Request, Response } from 'express'; + +import { stdout } from './shell'; + +export const assertAuthentication: (options?: { + failureRedirect?: string; + failureReturnTo?: boolean | string; +}) => Handler = ({ failureRedirect, failureReturnTo } = {}) => { + const redirectOnFailure: (response: Response) => void = failureRedirect + ? (response) => response.redirect(failureRedirect) + : (response) => response.status(404).send(); + + let getSessionReturnTo: ((request: Request) => string) | undefined; + + if (failureReturnTo === true) { + getSessionReturnTo = ({ originalUrl, url }) => originalUrl || url; + } else if (typeof failureReturnTo === 'string') { + getSessionReturnTo = () => failureReturnTo; + } + + return (request, response, next) => { + const { originalUrl, session } = request; + const { passport } = session; + + if (!passport?.user) { + session.returnTo = getSessionReturnTo?.call(null, request); + + stdout( + `Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, + ); + + return redirectOnFailure?.call(null, response); + } + + next(); + }; +}; From d8a2c77cf0752d41ac8ed4f3f52b72a5b68668ba Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 20:35:06 -0400 Subject: [PATCH 36/81] fix(striker-ui-api): add default authentication handler for private /api/* --- striker-ui-api/src/lib/assertAuthentication.ts | 2 ++ striker-ui-api/src/routes/auth.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/assertAuthentication.ts b/striker-ui-api/src/lib/assertAuthentication.ts index a49d7889..96db0023 100644 --- a/striker-ui-api/src/lib/assertAuthentication.ts +++ b/striker-ui-api/src/lib/assertAuthentication.ts @@ -35,3 +35,5 @@ export const assertAuthentication: (options?: { next(); }; }; + +export const authenticationHandler = assertAuthentication(); diff --git a/striker-ui-api/src/routes/auth.ts b/striker-ui-api/src/routes/auth.ts index 2be436ef..1a2af236 100644 --- a/striker-ui-api/src/routes/auth.ts +++ b/striker-ui-api/src/routes/auth.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { assertAuthentication } from '../lib/assertAuthentication'; +import { authenticationHandler } from '../lib/assertAuthentication'; import { login, logout } from '../lib/request_handlers/auth'; import passport from '../passport'; @@ -8,6 +8,6 @@ const router = express.Router(); router .post('/login', passport.authenticate('login'), login) - .put('/logout', assertAuthentication(), logout); + .put('/logout', authenticationHandler, logout); export default router; From 9170f786504fa8b4a7c613bd65d0084eccf3503c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 18 Apr 2023 20:36:25 -0400 Subject: [PATCH 37/81] fix(striker-ui-api): make static files available through root --- striker-ui-api/src/app.ts | 13 ++++++------- striker-ui-api/src/lib/consts/SERVER_PATHS.ts | 1 + striker-ui-api/src/routes/index.ts | 10 +++++++--- striker-ui-api/src/routes/static.ts | 13 +++++++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 striker-ui-api/src/routes/static.ts diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index feea2742..38d7f790 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,7 +1,7 @@ import cors from 'cors'; import express, { json } from 'express'; -import { assertAuthentication } from './lib/assertAuthentication'; +import { authenticationHandler } from './lib/assertAuthentication'; import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; @@ -20,13 +20,12 @@ app.use(sessionHandler); app.use(passport.initialize()); app.use(passport.authenticate('session')); -const authenticationHandler = assertAuthentication(); - -rrouters(app, routes, { +rrouters(app, routes.private, { assign: (router) => [authenticationHandler, router], - key: 'api', + route: '/api', }); -rrouters(app, routes, { key: 'auth' }); -rrouters(app, routes, { key: 'echo' }); +rrouters(app, routes.public, { route: '/api' }); + +app.use(routes.static); export default app; diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index 2d089eef..ed2d7ffe 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -37,6 +37,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { 'striker-parse-os-list': {}, }, }, + var: { www: { html: {} } }, }; const generatePaths = ( diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index 29b3719a..1fb9f91c 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -10,11 +10,12 @@ import manifestRouter from './manifest'; import networkInterfaceRouter from './network-interface'; import serverRouter from './server'; import sshKeyRouter from './ssh-key'; +import staticRouter from './static'; import upsRouter from './ups'; import userRouter from './user'; const routes = { - api: { + private: { anvil: anvilRouter, command: commandRouter, fence: fenceRouter, @@ -28,8 +29,11 @@ const routes = { ups: upsRouter, user: userRouter, }, - auth: authRouter, - echo: echoRouter, + public: { + auth: authRouter, + echo: echoRouter, + }, + static: staticRouter, }; export default routes; diff --git a/striker-ui-api/src/routes/static.ts b/striker-ui-api/src/routes/static.ts new file mode 100644 index 00000000..c06579e2 --- /dev/null +++ b/striker-ui-api/src/routes/static.ts @@ -0,0 +1,13 @@ +import express from 'express'; + +import { SERVER_PATHS } from '../lib/consts'; + +const router = express.Router(); + +router.use( + express.static(SERVER_PATHS.var.www.html.self, { + extensions: ['htm', 'html'], + }), +); + +export default router; From cbf6ac0ff9e0bbbaabb38a1cdc63017b254bac17 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 13:04:24 -0400 Subject: [PATCH 38/81] refactor(striker-ui-api): rename authenticationHandler->guardApi --- striker-ui-api/src/app.ts | 4 ++-- striker-ui-api/src/lib/assertAuthentication.ts | 2 +- striker-ui-api/src/routes/auth.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 38d7f790..0bd58474 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -1,7 +1,7 @@ import cors from 'cors'; import express, { json } from 'express'; -import { authenticationHandler } from './lib/assertAuthentication'; +import { guardApi } from './lib/assertAuthentication'; import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; @@ -21,7 +21,7 @@ app.use(passport.initialize()); app.use(passport.authenticate('session')); rrouters(app, routes.private, { - assign: (router) => [authenticationHandler, router], + assign: (router) => [guardApi, router], route: '/api', }); rrouters(app, routes.public, { route: '/api' }); diff --git a/striker-ui-api/src/lib/assertAuthentication.ts b/striker-ui-api/src/lib/assertAuthentication.ts index 96db0023..4abfb58f 100644 --- a/striker-ui-api/src/lib/assertAuthentication.ts +++ b/striker-ui-api/src/lib/assertAuthentication.ts @@ -36,4 +36,4 @@ export const assertAuthentication: (options?: { }; }; -export const authenticationHandler = assertAuthentication(); +export const guardApi = assertAuthentication(); diff --git a/striker-ui-api/src/routes/auth.ts b/striker-ui-api/src/routes/auth.ts index 1a2af236..5ffb8491 100644 --- a/striker-ui-api/src/routes/auth.ts +++ b/striker-ui-api/src/routes/auth.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { authenticationHandler } from '../lib/assertAuthentication'; +import { guardApi } from '../lib/assertAuthentication'; import { login, logout } from '../lib/request_handlers/auth'; import passport from '../passport'; @@ -8,6 +8,6 @@ const router = express.Router(); router .post('/login', passport.authenticate('login'), login) - .put('/logout', authenticationHandler, logout); + .put('/logout', guardApi, logout); export default router; From 74175c538a20081734a6e38d0943c88e48f9ce1d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 13:10:27 -0400 Subject: [PATCH 39/81] refactor(striker-ui-api): remove unused import in passport --- striker-ui-api/src/passport.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts index b9c3fe1e..1ebcdbf0 100644 --- a/striker-ui-api/src/passport.ts +++ b/striker-ui-api/src/passport.ts @@ -1,4 +1,3 @@ -import { Express } from 'express'; import passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; From 46891f322be23fb726218107f914a675009fb066 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 13:18:01 -0400 Subject: [PATCH 40/81] refactor(striker-ui-api): rename session->expressSession, sessionHandler->session --- striker-ui-api/src/app.ts | 4 ++-- striker-ui-api/src/session.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 0bd58474..32f212b6 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -5,7 +5,7 @@ import { guardApi } from './lib/assertAuthentication'; import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; -import sessionHandler from './session'; +import session from './session'; const app = express(); @@ -15,7 +15,7 @@ app.use(cors()); // Add session handler to the chain **after** adding other handlers that do // not depend on session(s). -app.use(sessionHandler); +app.use(session); app.use(passport.initialize()); app.use(passport.authenticate('session')); diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index ca22978d..aee10b8b 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -1,4 +1,4 @@ -import session, { +import expressSession, { SessionData, Store as BaseSessionStore, } from 'express-session'; @@ -13,7 +13,7 @@ import { getSessionSecret } from './lib/getSessionSecret'; import { isObject } from './lib/isObject'; import { stderr, stdout, stdoutVar, uuidgen } from './lib/shell'; -const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 1000 * 60 * 60; +const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 3600000; const getWriteCode = (obj: object) => { let result: number | undefined; @@ -218,7 +218,7 @@ export class SessionStore extends BaseSessionStore { } } -const sessionHandler = session({ +const session = expressSession({ cookie: { maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE }, genid: ({ path }) => { const sid = uuidgen('--random').trim(); @@ -233,4 +233,4 @@ const sessionHandler = session({ store: new SessionStore(), }); -export default sessionHandler; +export default session; From 5c7ddbfb5e9cd802792902a7aa7defd629bf67f4 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 14:33:18 -0400 Subject: [PATCH 41/81] refactor(striker-ui-api): group API-related types by endpoint --- .../request_handlers/command/getHostSSH.ts | 6 +- .../host/getHostConnection.ts | 6 +- .../ssh-key/deleteSSHKeyConflict.ts | 2 +- .../ssh-key/getSSHKeyConflict.ts | 11 +-- .../src/lib/request_handlers/ups/getUPS.ts | 2 +- .../request_handlers/ups/getUPSTemplate.ts | 4 +- striker-ui-api/src/types/AnvilOverview.d.ts | 8 -- .../types/{AnvilDetail.d.ts => ApiAn.d.ts} | 9 ++ .../src/types/{APIAuth.d.ts => ApiAuth.d.ts} | 0 .../types/{APIFence.d.ts => ApiFence.d.ts} | 0 striker-ui-api/src/types/ApiHost.d.ts | 89 +++++++++++++++++++ .../{APIManifest.d.ts => ApiManifest.d.ts} | 0 ...Overview.d.ts => ApiNetworkInterface.d.ts} | 0 .../{ServerOverview.d.ts => ApiServer.d.ts} | 0 striker-ui-api/src/types/ApiSshKey.d.ts | 12 +++ .../src/types/{APIUPS.d.ts => ApiUps.d.ts} | 4 +- striker-ui-api/src/types/ApiUser.d.ts | 7 ++ .../CreateHostConnectionRequestBody.d.ts | 11 --- .../DeleteHostConnectionRequestBody.d.ts | 3 - .../DeleteSSHKeyConflictRequestBody.d.ts | 1 - .../src/types/HostConnectionOverview.d.ts | 25 ------ striker-ui-api/src/types/HostOverview.d.ts | 6 -- .../src/types/InitializeStrikerForm.d.ts | 19 ---- .../src/types/UpdateHostRequestBody.d.ts | 20 ----- 24 files changed, 130 insertions(+), 115 deletions(-) delete mode 100644 striker-ui-api/src/types/AnvilOverview.d.ts rename striker-ui-api/src/types/{AnvilDetail.d.ts => ApiAn.d.ts} (85%) rename striker-ui-api/src/types/{APIAuth.d.ts => ApiAuth.d.ts} (100%) rename striker-ui-api/src/types/{APIFence.d.ts => ApiFence.d.ts} (100%) create mode 100644 striker-ui-api/src/types/ApiHost.d.ts rename striker-ui-api/src/types/{APIManifest.d.ts => ApiManifest.d.ts} (100%) rename striker-ui-api/src/types/{NetworkInterfaceOverview.d.ts => ApiNetworkInterface.d.ts} (100%) rename striker-ui-api/src/types/{ServerOverview.d.ts => ApiServer.d.ts} (100%) create mode 100644 striker-ui-api/src/types/ApiSshKey.d.ts rename striker-ui-api/src/types/{APIUPS.d.ts => ApiUps.d.ts} (85%) create mode 100644 striker-ui-api/src/types/ApiUser.d.ts delete mode 100644 striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts delete mode 100644 striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts delete mode 100644 striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts delete mode 100644 striker-ui-api/src/types/HostConnectionOverview.d.ts delete mode 100644 striker-ui-api/src/types/HostOverview.d.ts delete mode 100644 striker-ui-api/src/types/InitializeStrikerForm.d.ts delete mode 100644 striker-ui-api/src/types/UpdateHostRequestBody.d.ts diff --git a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts index 5b359625..258d9f49 100644 --- a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts +++ b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts @@ -9,7 +9,7 @@ import { stderr } from '../../shell'; export const getHostSSH: RequestHandler< unknown, { - badSSHKeys?: DeleteSSHKeyConflictRequestBody; + badSSHKeys?: DeleteSshKeyConflictRequestBody; hostName: string; hostOS: string; hostUUID: string; @@ -53,7 +53,7 @@ export const getHostSSH: RequestHandler< return; } - let badSSHKeys: DeleteSSHKeyConflictRequestBody | undefined; + let badSSHKeys: DeleteSshKeyConflictRequestBody | undefined; if (!isConnected) { const rows = dbQuery(` @@ -65,7 +65,7 @@ export const getHostSSH: RequestHandler< )}';`).stdout as [stateNote: string, stateUUID: string][]; if (rows.length > 0) { - badSSHKeys = rows.reduce( + badSSHKeys = rows.reduce( (previous, [, stateUUID]) => { previous[localHostUUID].push(stateUUID); diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index cf5e5453..0bf8da69 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -59,9 +59,9 @@ export const getHostConnection = buildGetRequestHandler( stdout(`condHostUUIDs=[${condHostUUIDs}]`); try { - ({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>( - { database: true }, - )); + ({ database: rawDatabaseData } = getAnvilData<{ + database: AnvilDataDatabaseHash; + }>({ database: true })); } catch (subError) { throw new Error(`Failed to get anvil data; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts index c4144efb..f2e634c8 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts @@ -9,7 +9,7 @@ import { stderr } from '../../shell'; export const deleteSSHKeyConflict: RequestHandler< unknown, undefined, - DeleteSSHKeyConflictRequestBody + DeleteSshKeyConflictRequestBody > = (request, response) => { const { body } = request; const hostUUIDs = Object.keys(body); diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts index fcb29b94..d808e64c 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts @@ -22,16 +22,7 @@ export const getSSHKeyConflict = buildGetRequestHandler( ON sta.state_host_uuid = hos.host_uuid WHERE sta.state_name LIKE '${HOST_KEY_CHANGED_PREFIX}%';`; const afterQueryReturn = buildQueryResultReducer<{ - [hostUUID: string]: { - [stateUUID: string]: { - badFile: string; - badLine: number; - hostName: string; - hostUUID: string; - ipAddress: string; - stateUUID: string; - }; - }; + [hostUUID: string]: SshKeyConflict; }>((previous, [hostName, hostUUID, stateName, stateNote, stateUUID]) => { const hostUUIDKey = toLocal(hostUUID, localHostUUID); diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts index e884e54f..099cdbc8 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts @@ -14,7 +14,7 @@ export const getUPS: RequestHandler = buildGetRequestHandler( FROM upses ORDER BY ups_name ASC;`; const afterQueryReturn: QueryResultModifierFunction | undefined = - buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>( + buildQueryResultReducer<{ [upsUUID: string]: UpsOverview }>( (previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => { previous[upsUUID] = { upsAgent, diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index 3c4a8b9c..395e46b7 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -21,13 +21,13 @@ export const getUPSTemplate: RequestHandler = (request, response) => { const upsData: AnvilDataUPSHash = Object.entries( rawUPSData, - ).reduce((previous, [upsTypeId, value]) => { + ).reduce((previous, [upsTypeId, value]) => { const { brand, description: rawDescription, ...rest } = value; const matched = rawDescription.match( /^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/, ); - const result: UPSTemplate[string] = { + const result: UpsTemplate[string] = { ...rest, brand, description: rawDescription, diff --git a/striker-ui-api/src/types/AnvilOverview.d.ts b/striker-ui-api/src/types/AnvilOverview.d.ts deleted file mode 100644 index 2b37b418..00000000 --- a/striker-ui-api/src/types/AnvilOverview.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -type AnvilOverview = { - anvilName: string; - anvilUUID: string; - hosts: Array<{ - hostName: string; - hostUUID: string; - }>; -}; diff --git a/striker-ui-api/src/types/AnvilDetail.d.ts b/striker-ui-api/src/types/ApiAn.d.ts similarity index 85% rename from striker-ui-api/src/types/AnvilDetail.d.ts rename to striker-ui-api/src/types/ApiAn.d.ts index 55261544..d49b8d61 100644 --- a/striker-ui-api/src/types/AnvilDetail.d.ts +++ b/striker-ui-api/src/types/ApiAn.d.ts @@ -31,3 +31,12 @@ type AnvilDetailForProvisionServer = { fileName: string; }>; }; + +type AnvilOverview = { + anvilName: string; + anvilUUID: string; + hosts: Array<{ + hostName: string; + hostUUID: string; + }>; +}; diff --git a/striker-ui-api/src/types/APIAuth.d.ts b/striker-ui-api/src/types/ApiAuth.d.ts similarity index 100% rename from striker-ui-api/src/types/APIAuth.d.ts rename to striker-ui-api/src/types/ApiAuth.d.ts diff --git a/striker-ui-api/src/types/APIFence.d.ts b/striker-ui-api/src/types/ApiFence.d.ts similarity index 100% rename from striker-ui-api/src/types/APIFence.d.ts rename to striker-ui-api/src/types/ApiFence.d.ts diff --git a/striker-ui-api/src/types/ApiHost.d.ts b/striker-ui-api/src/types/ApiHost.d.ts new file mode 100644 index 00000000..e9127590 --- /dev/null +++ b/striker-ui-api/src/types/ApiHost.d.ts @@ -0,0 +1,89 @@ +type CreateHostConnectionRequestBody = { + dbName?: string; + ipAddress: string; + isPing?: boolean; + /** Host password; same as database password */ + password: string; + port?: number; + sshPort?: number; + /** Database user */ + user?: string; +}; + +type DeleteHostConnectionRequestBody = { + [hostUUID: string]: string[]; +}; + +type HostConnectionOverview = { + inbound: { + ipAddress: { + [ipAddress: string]: { + hostUUID: string; + ipAddress: string; + ipAddressUUID: string; + networkLinkNumber: number; + networkNumber: number; + networkType: string; + }; + }; + port: number; + user: string; + }; + peer: { + [ipAddress: string]: { + hostUUID: string; + ipAddress: string; + isPing: boolean; + port: number; + user: string; + }; + }; +}; + +type HostOverview = { + hostName: string; + hostType: string; + hostUUID: string; + shortHostName: string; +}; + +type InitializeStrikerNetworkForm = { + interfaces: Array; + ipAddress: string; + name: string; + subnetMask: string; + type: string; +}; + +type InitializeStrikerForm = { + adminPassword: string; + domainName: string; + hostName: string; + hostNumber: number; + networkDNS: string; + networkGateway: string; + networks: InitializeStrikerNetworkForm[]; + organizationName: string; + organizationPrefix: string; +}; + +type PrepareHostRequestBody = { + enterpriseUUID?: string; + hostIPAddress: string; + hostName: string; + hostPassword: string; + hostSSHPort?: number; + hostType: string; + hostUser?: string; + hostUUID?: string; + redhatPassword: string; + redhatUser: string; +}; + +type SetHostInstallTargetRequestBody = { + isEnableInstallTarget: boolean; +}; + +type UpdateHostParams = { + hostUUID: string; +}; diff --git a/striker-ui-api/src/types/APIManifest.d.ts b/striker-ui-api/src/types/ApiManifest.d.ts similarity index 100% rename from striker-ui-api/src/types/APIManifest.d.ts rename to striker-ui-api/src/types/ApiManifest.d.ts diff --git a/striker-ui-api/src/types/NetworkInterfaceOverview.d.ts b/striker-ui-api/src/types/ApiNetworkInterface.d.ts similarity index 100% rename from striker-ui-api/src/types/NetworkInterfaceOverview.d.ts rename to striker-ui-api/src/types/ApiNetworkInterface.d.ts diff --git a/striker-ui-api/src/types/ServerOverview.d.ts b/striker-ui-api/src/types/ApiServer.d.ts similarity index 100% rename from striker-ui-api/src/types/ServerOverview.d.ts rename to striker-ui-api/src/types/ApiServer.d.ts diff --git a/striker-ui-api/src/types/ApiSshKey.d.ts b/striker-ui-api/src/types/ApiSshKey.d.ts new file mode 100644 index 00000000..956cf6e1 --- /dev/null +++ b/striker-ui-api/src/types/ApiSshKey.d.ts @@ -0,0 +1,12 @@ +type SshKeyConflict = { + [stateUUID: string]: { + badFile: string; + badLine: number; + hostName: string; + hostUUID: string; + ipAddress: string; + stateUUID: string; + }; +}; + +type DeleteSshKeyConflictRequestBody = { [hostUUID: string]: string[] }; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/ApiUps.d.ts similarity index 85% rename from striker-ui-api/src/types/APIUPS.d.ts rename to striker-ui-api/src/types/ApiUps.d.ts index 5199e348..c8ded148 100644 --- a/striker-ui-api/src/types/APIUPS.d.ts +++ b/striker-ui-api/src/types/ApiUps.d.ts @@ -1,11 +1,11 @@ -type UPSOverview = { +type UpsOverview = { upsAgent: string; upsIPAddress: string; upsName: string; upsUUID: string; }; -type UPSTemplate = { +type UpsTemplate = { [upsName: string]: AnvilDataUPSHash[string] & { links: { [linkId: string]: { diff --git a/striker-ui-api/src/types/ApiUser.d.ts b/striker-ui-api/src/types/ApiUser.d.ts new file mode 100644 index 00000000..213a6871 --- /dev/null +++ b/striker-ui-api/src/types/ApiUser.d.ts @@ -0,0 +1,7 @@ +type DeleteUserParamsDictionary = { + userUuid: string; +}; + +type DeleteUserRequestBody = { + uuids?: string[]; +}; diff --git a/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts deleted file mode 100644 index ce50d3a2..00000000 --- a/striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -type CreateHostConnectionRequestBody = { - dbName?: string; - ipAddress: string; - isPing?: boolean; - // Host password; same as database password. - password: string; - port?: number; - sshPort?: number; - // database user. - user?: string; -}; diff --git a/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts b/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts deleted file mode 100644 index cea743b6..00000000 --- a/striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DeleteHostConnectionRequestBody = { - [hostUUID: string]: string[]; -}; diff --git a/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts b/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts deleted file mode 100644 index f2451843..00000000 --- a/striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts +++ /dev/null @@ -1 +0,0 @@ -type DeleteSSHKeyConflictRequestBody = { [hostUUID: string]: string[] }; diff --git a/striker-ui-api/src/types/HostConnectionOverview.d.ts b/striker-ui-api/src/types/HostConnectionOverview.d.ts deleted file mode 100644 index 71c405bd..00000000 --- a/striker-ui-api/src/types/HostConnectionOverview.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -type HostConnectionOverview = { - inbound: { - ipAddress: { - [ipAddress: string]: { - hostUUID: string; - ipAddress: string; - ipAddressUUID: string; - networkLinkNumber: number; - networkNumber: number; - networkType: string; - }; - }; - port: number; - user: string; - }; - peer: { - [ipAddress: string]: { - hostUUID: string; - ipAddress: string; - isPing: boolean; - port: number; - user: string; - }; - }; -}; diff --git a/striker-ui-api/src/types/HostOverview.d.ts b/striker-ui-api/src/types/HostOverview.d.ts deleted file mode 100644 index 8e991dd3..00000000 --- a/striker-ui-api/src/types/HostOverview.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -type HostOverview = { - hostName: string; - hostType: string; - hostUUID: string; - shortHostName: string; -}; diff --git a/striker-ui-api/src/types/InitializeStrikerForm.d.ts b/striker-ui-api/src/types/InitializeStrikerForm.d.ts deleted file mode 100644 index 64a46c7b..00000000 --- a/striker-ui-api/src/types/InitializeStrikerForm.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -type InitializeStrikerNetworkForm = { - interfaces: Array; - ipAddress: string; - name: string; - subnetMask: string; - type: string; -}; - -type InitializeStrikerForm = { - adminPassword: string; - domainName: string; - hostName: string; - hostNumber: number; - networkDNS: string; - networkGateway: string; - networks: InitializeStrikerNetworkForm[]; - organizationName: string; - organizationPrefix: string; -}; diff --git a/striker-ui-api/src/types/UpdateHostRequestBody.d.ts b/striker-ui-api/src/types/UpdateHostRequestBody.d.ts deleted file mode 100644 index a518e2d8..00000000 --- a/striker-ui-api/src/types/UpdateHostRequestBody.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -type SetHostInstallTargetRequestBody = { - isEnableInstallTarget: boolean; -}; - -type PrepareHostRequestBody = { - enterpriseUUID?: string; - hostIPAddress: string; - hostName: string; - hostPassword: string; - hostSSHPort?: number; - hostType: string; - hostUser?: string; - hostUUID?: string; - redhatPassword: string; - redhatUser: string; -}; - -type UpdateHostParams = { - hostUUID: string; -}; From d9e38d87e206ec25e5086551ae5b19f8c90d4345 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 16:22:36 -0400 Subject: [PATCH 42/81] fix(striker-ui-api): include write code in async database write close handler args --- striker-ui-api/src/lib/accessModule.ts | 29 +++++++++++++++---- striker-ui-api/src/session.ts | 23 ++------------- striker-ui-api/src/types/AccessModule.d.ts | 33 ++++++++++++++++++---- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 92335c57..ee710981 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -4,6 +4,7 @@ import { readFileSync } from 'fs'; import { SERVER_PATHS } from './consts'; import { formatSql } from './formatSql'; +import { isObject } from './isObject'; import { date, stderr as sherr, stdout as shout } from './shell'; const asyncAnvilAccessModule = ( @@ -190,13 +191,31 @@ const dbSubRefreshTimestamp = () => { return result; }; -const awrite = (script: string, options?: AsyncAnvilAccessModuleOptions) => { +const awrite = ( + script: string, + { onClose: initOnClose, ...restOptions }: AsyncDatabaseWriteOptions = {}, +) => { shout(formatSql(script)); - return asyncAnvilAccessModule( - ['--query', script, '--mode', 'write'], - options, - ); + const onClose: AsyncAnvilAccessModuleCloseHandler = (args, ...rest) => { + const { stdout } = args; + const { obj: output } = isObject(stdout); + + let wcode: number | null = null; + + if ('write_code' in output) { + ({ write_code: wcode } = output as { write_code: number }); + + shout(`Async write completed with write_code=${wcode}`); + } + + initOnClose?.call(null, { wcode, ...args }, ...rest); + }; + + return asyncAnvilAccessModule(['--query', script, '--mode', 'write'], { + onClose, + ...restOptions, + }); }; const dbWrite = (script: string, options?: SpawnSyncOptions) => { diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index aee10b8b..ce769bda 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -10,21 +10,10 @@ import { timestamp, } from './lib/accessModule'; import { getSessionSecret } from './lib/getSessionSecret'; -import { isObject } from './lib/isObject'; import { stderr, stdout, stdoutVar, uuidgen } from './lib/shell'; const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 3600000; -const getWriteCode = (obj: object) => { - let result: number | undefined; - - if ('write_code' in obj) { - ({ write_code: result } = obj as { write_code: number }); - } - - return result; -}; - export class SessionStore extends BaseSessionStore { constructor(options = {}) { super(options); @@ -38,9 +27,7 @@ export class SessionStore extends BaseSessionStore { try { awrite(`DELETE FROM sessions WHERE session_uuid = '${sid}';`, { - onClose({ stdout: s1 }) { - const wcode = getWriteCode(isObject(s1).obj); - + onClose({ wcode }) { if (wcode !== 0) { stderr( `SQL script failed during destroy session ${sid}; code: ${wcode}`, @@ -147,9 +134,7 @@ export class SessionStore extends BaseSessionStore { DO UPDATE SET session_host_uuid = '${localHostUuid}', modified_date = '${modifiedDate}';`, { - onClose: ({ stdout: s1 }) => { - const wcode = getWriteCode(isObject(s1).obj); - + onClose: ({ wcode }) => { if (wcode !== 0) { stderr( `SQL script failed during set session ${sid}; code: ${wcode}`, @@ -181,9 +166,7 @@ export class SessionStore extends BaseSessionStore { awrite( `UPDATE sessions SET modified_date = '${timestamp()}' WHERE session_uuid = '${sid}';`, { - onClose: ({ stdout: s1 }) => { - const wcode = getWriteCode(isObject(s1).obj); - + onClose: ({ wcode }) => { if (wcode !== 0) { stderr( `SQL script failed during touch session ${sid}; code: ${wcode}`, diff --git a/striker-ui-api/src/types/AccessModule.d.ts b/striker-ui-api/src/types/AccessModule.d.ts index d5b00ed7..c95732e6 100644 --- a/striker-ui-api/src/types/AccessModule.d.ts +++ b/striker-ui-api/src/types/AccessModule.d.ts @@ -1,9 +1,30 @@ +type AsyncAnvilAccessModuleCloseArgs = { + ecode: number | null; + signal: NodeJS.Signals | null; + stderr: string; + stdout: unknown; +}; + +type AsyncDatabaseWriteCloseArgs = AsyncAnvilAccessModuleCloseArgs & { + wcode: number | null; +}; + +type AsyncAnvilAccessModuleCloseHandler = ( + args: AsyncAnvilAccessModuleCloseArgs, +) => void; + +type AsyncDatabaseWriteCloseHandler = ( + args: AsyncDatabaseWriteCloseArgs, +) => void; + type AsyncAnvilAccessModuleOptions = import('child_process').SpawnOptions & { - onClose?: (args: { - ecode: number | null; - signal: NodeJS.Signals | null; - stderr: string; - stdout: unknown; - }) => void; + onClose?: AsyncAnvilAccessModuleCloseHandler; onError?: (err: Error) => void; }; + +type AsyncDatabaseWriteOptions = Omit< + AsyncAnvilAccessModuleOptions, + 'onClose' +> & { + onClose?: AsyncDatabaseWriteCloseHandler; +}; From 414c4d64c65c98ea19b8b51f6f7437d7e570f69f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 16:26:39 -0400 Subject: [PATCH 43/81] fix(striker-ui-api): add delete user request handler --- striker-ui-api/src/lib/consts/DELETED.ts | 1 + striker-ui-api/src/lib/consts/index.ts | 2 + .../lib/request_handlers/user/deleteUser.ts | 69 +++++++++++++++++++ .../src/lib/request_handlers/user/index.ts | 1 + striker-ui-api/src/routes/user.ts | 7 +- 5 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 striker-ui-api/src/lib/consts/DELETED.ts create mode 100644 striker-ui-api/src/lib/request_handlers/user/deleteUser.ts diff --git a/striker-ui-api/src/lib/consts/DELETED.ts b/striker-ui-api/src/lib/consts/DELETED.ts new file mode 100644 index 00000000..68afc4e5 --- /dev/null +++ b/striker-ui-api/src/lib/consts/DELETED.ts @@ -0,0 +1 @@ +export const DELETED = 'DELETED'; diff --git a/striker-ui-api/src/lib/consts/index.ts b/striker-ui-api/src/lib/consts/index.ts index ee112bfe..8f37097d 100644 --- a/striker-ui-api/src/lib/consts/index.ts +++ b/striker-ui-api/src/lib/consts/index.ts @@ -3,6 +3,8 @@ import SERVER_PATHS from './SERVER_PATHS'; export { SERVER_PATHS }; export * from './AN_VARIABLE_NAME_LIST'; +export * from './DELETED'; export * from './EXIT_CODE_LIST'; export * from './PROCESS_OWNER'; +export * from './REG_EXP_PATTERNS'; export * from './SERVER_PORT'; diff --git a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts new file mode 100644 index 00000000..64008967 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts @@ -0,0 +1,69 @@ +import assert from 'assert'; +import { RequestHandler } from 'express'; + +import { DELETED, REP_UUID } from '../../consts'; + +import { awrite } from '../../accessModule'; +import join from '../../join'; +import { sanitize } from '../../sanitize'; +import { stderr, stdoutVar } from '../../shell'; + +export const deleteUser: RequestHandler< + DeleteUserParamsDictionary, + undefined, + DeleteUserRequestBody +> = (request, response) => { + const { + body: { uuids: rawUserUuidList } = {}, + params: { userUuid }, + } = request; + + const userUuidList = sanitize(rawUserUuidList, 'string[]'); + + const ulist = userUuidList.length > 0 ? userUuidList : [userUuid]; + + stdoutVar({ ulist }); + + try { + let failedIndex = 0; + + assert( + ulist.every((uuid, index) => { + failedIndex = index; + + return REP_UUID.test(uuid); + }), + `All UUIDs must be valid UUIDv4; failed at ${failedIndex}, got [${ulist[failedIndex]}]`, + ); + } catch (assertError) { + stderr(`Failed to assert value during delete user; CAUSE: ${assertError}`); + + return response.status(400).send(); + } + + try { + awrite( + `UPDATE users + SET user_algorithm = '${DELETED}' + WHERE user_uuid IN (${join(ulist)});`, + { + onClose: ({ ecode, wcode }) => { + if (ecode !== 0 || wcode !== 0) { + stderr( + `SQL script failed in delete user(s); ecode=${ecode}, wcode=${wcode}`, + ); + } + }, + onError: (error) => { + stderr(`Delete user subprocess error; CAUSE: ${error}`); + }, + }, + ); + } catch (error) { + stderr(`Failed to delete user(s); CAUSE: ${error}`); + + return response.status(500).send(); + } + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/user/index.ts b/striker-ui-api/src/lib/request_handlers/user/index.ts index 3f9351e9..2649ee0e 100644 --- a/striker-ui-api/src/lib/request_handlers/user/index.ts +++ b/striker-ui-api/src/lib/request_handlers/user/index.ts @@ -1 +1,2 @@ +export * from './deleteUser'; export * from './getUser'; diff --git a/striker-ui-api/src/routes/user.ts b/striker-ui-api/src/routes/user.ts index a3633efa..4cc94a06 100644 --- a/striker-ui-api/src/routes/user.ts +++ b/striker-ui-api/src/routes/user.ts @@ -1,9 +1,12 @@ import express from 'express'; -import { getUser } from '../lib/request_handlers/user'; +import { deleteUser, getUser } from '../lib/request_handlers/user'; const router = express.Router(); -router.get('/', getUser); +router + .get('/', getUser) + .delete('/', deleteUser) + .delete('/:userUuid', deleteUser); export default router; From 089efdbdc3bb15b543bf72871895898f3ef92f53 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 19 Apr 2023 17:37:37 -0400 Subject: [PATCH 44/81] fix(striker-ui): add GatePanel and login page --- striker-ui/components/GateForm.tsx | 13 +++---- striker-ui/components/GatePanel.tsx | 53 +++++++++++++++++++++++++++++ striker-ui/pages/login/index.tsx | 17 +++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 striker-ui/components/GatePanel.tsx create mode 100644 striker-ui/pages/login/index.tsx diff --git a/striker-ui/components/GateForm.tsx b/striker-ui/components/GateForm.tsx index 6a8e723c..b7c81841 100644 --- a/striker-ui/components/GateForm.tsx +++ b/striker-ui/components/GateForm.tsx @@ -1,11 +1,5 @@ import { Box, BoxProps, SxProps, Theme } from '@mui/material'; -import { - forwardRef, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react'; +import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'; import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; @@ -18,6 +12,7 @@ import OutlinedInputWithLabel from './OutlinedInputWithLabel'; import Spinner from './Spinner'; import { buildPeacefulStringTestBatch } from '../lib/test_input'; import useFormUtils from '../hooks/useFormUtils'; +import useProtectedState from '../hooks/useProtectedState'; const INPUT_ROOT_SX: SxProps = { width: '100%' }; @@ -72,7 +67,7 @@ const GateForm = forwardRef( const inputPassphraseRef = useRef>({}); const messageGroupRef = useRef({}); - const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitting, setIsSubmitting] = useProtectedState(false); const formUtils = useFormUtils( [INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE], @@ -118,7 +113,7 @@ const GateForm = forwardRef( ...args, ); }), - [onSubmit, onSubmitAppend, setMessage], + [onSubmit, onSubmitAppend, setIsSubmitting, setMessage], ); const submitElement = useMemo( diff --git a/striker-ui/components/GatePanel.tsx b/striker-ui/components/GatePanel.tsx new file mode 100644 index 00000000..ec483387 --- /dev/null +++ b/striker-ui/components/GatePanel.tsx @@ -0,0 +1,53 @@ +import { useRouter } from 'next/router'; +import { FC } from 'react'; + +import api from '../lib/api'; +import GateForm from './GateForm'; +import handleAPIError from '../lib/handleAPIError'; +import { Panel } from './Panels'; + +const GatePanel: FC = () => { + const router = useRouter(); + + return ( + + { + setIsSubmitting(true); + + api + .post('/auth/login', { username, password }) + .then(() => { + router.push('/'); + }) + .catch((error) => { + const emsg = handleAPIError(error, { + onResponseErrorAppend: () => ({ + children: `Credentials mismatched.`, + }), + }); + + setMessage(emsg); + }) + .finally(() => { + setIsSubmitting(false); + }); + }} + passphraseLabel="Passphrase" + submitLabel="Login" + /> + + ); +}; + +export default GatePanel; diff --git a/striker-ui/pages/login/index.tsx b/striker-ui/pages/login/index.tsx new file mode 100644 index 00000000..358b486a --- /dev/null +++ b/striker-ui/pages/login/index.tsx @@ -0,0 +1,17 @@ +import Head from 'next/head'; +import { FC } from 'react'; + +import GatePanel from '../../components/GatePanel'; +import Header from '../../components/Header'; + +const Login: FC = () => ( + <> + + Login + +
+ + +); + +export default Login; From d9bc73ec2d42356c0631c0dda367751f6c196151 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 21 Apr 2023 00:48:55 -0400 Subject: [PATCH 45/81] feat(tools): add script capability to anvil-access-module --- tools/anvil-access-module | 358 ++++++++++++++++++++++++++++---------- 1 file changed, 268 insertions(+), 90 deletions(-) diff --git a/tools/anvil-access-module b/tools/anvil-access-module index 7e428784..f13a0b86 100755 --- a/tools/anvil-access-module +++ b/tools/anvil-access-module @@ -116,114 +116,68 @@ use strict; use warnings; use Anvil::Tools; use JSON; -use Data::Dumper; +use Text::ParseWords; $| = 1; my $THIS_FILE = ($0 =~ /^.*\/(.*)$/)[0]; my $running_directory = ($0 =~ /^(.*?)\/$THIS_FILE$/)[0]; -if (($running_directory =~ /^\./) && ($ENV{PWD})) -{ - $running_directory =~ s/^\./$ENV{PWD}/; -} - -my $anvil = Anvil::Tools->new(); - -sub is_array -{ - return ref($_[0]) eq "ARRAY"; -} - -sub is_hash -{ - return ref($_[0]) eq "HASH"; -} -sub db_access -{ - my $parameters = shift; - my $db_access_mode = $parameters->{db_access_mode}; - my $db_uuid = $parameters->{db_uuid}; - my $sql_query = $parameters->{sql_query}; +$running_directory =~ s/^\./$ENV{PWD}/ if $running_directory =~ /^\./ && $ENV{PWD}; - my $access_parameters = { query => $sql_query, uuid => $db_uuid, source => $THIS_FILE, line => __LINE__ }; - - return ($db_access_mode eq "write") - ? { write_code => $anvil->Database->write($access_parameters) } - : $anvil->Database->query($access_parameters); -} +my $anvil = Anvil::Tools->new(); -sub call_pre_data_fns +sub access_chain { my $parameters = shift; - my $fns = $parameters->{fns}; + # Required: + my $chain_str = $parameters->{chain}; + # Optional: + my $chain_args = $parameters->{chain_args} // []; - if (is_array($fns)) + my @chain = split(/->|[.]/, $chain_str); + my $key_index = 0; + my $intermediate = $anvil; + my @results; + + foreach my $key (@chain) { - foreach my $fn_wrapper ( @{$fns} ) + my $is_intermediate_hash = is_hash($intermediate); + my $is_last_key = $key_index == $#chain; + + if ($is_intermediate_hash) # Left-hand is hash; treat it as reading data { - if (is_array($fn_wrapper)) + last if (not exists $intermediate->{$key}); + + if ($is_last_key) { - # The double dash ( // ) operator is similar to the or ( || ) - # operator; it tests for defined instead of true. + @results = ($intermediate->{$key}); - my $pre_chain = @{$fn_wrapper}[0] // ""; - my @fn_params = @{$fn_wrapper}[1..$#{$fn_wrapper}] // (); - my @chain = split(/->|,/, $pre_chain); - my $intermediate = $anvil; - my $key_index = 0; - - foreach my $key ( @chain ) - { - last if not defined $intermediate->${key}; - - if ($key_index == $#chain && $intermediate->can($key)) - { - eval { $intermediate->${key}(@fn_params); }; - } - else - { - $intermediate = $intermediate->${key}; - } - - $key_index += 1; - } + last; } - } - } -} -sub get_anvil_data -{ - my $parameters = shift; - my $chain = $parameters->{chain}; - - my $source_intermediate = $anvil->data; - my $target_intermediate = $parameters->{data}; - my $key_index = 0; - - foreach my $key ( @{$chain} ) - { - last if not exists $source_intermediate->{$key}; + $intermediate = $intermediate->{$key}; + } + else # Left-hand is not hash; treat it as blessed/class object (module) and try to call a method from it + { + # Key not found in object; stop following the chain + last if (not defined $intermediate->${key}); - $source_intermediate = $source_intermediate->{$key}; + # On the last key of the chain; try to execute the subroutine if it exists + if ( $is_last_key && $intermediate->can($key) ) + { + eval { (@results) = $intermediate->${key}(@$chain_args); return 1; } or @results = (1); - if (not exists $target_intermediate->{$key}) - { - $target_intermediate->{$key} = {}; - } + last; + } - if ($key_index < $#{$chain}) - { - $target_intermediate = $target_intermediate->{$key}; - } - else - { - $target_intermediate->{$key} = $source_intermediate; + $intermediate = $intermediate->${key}; } $key_index += 1; } + + return (@results); } sub call_fn @@ -248,6 +202,45 @@ sub call_fn } } +sub call_pre_data_fns +{ + my $parameters = shift; + my $fns = $parameters->{fns}; + + if (is_array($fns)) + { + foreach my $fn_wrapper ( @{$fns} ) + { + if (is_array($fn_wrapper)) + { + # The double dash ( // ) operator is similar to the or ( || ) + # operator; it tests for defined instead of true. + + my @cargs = @{$fn_wrapper}[1..$#{$fn_wrapper}]; + + access_chain({ + chain => @{$fn_wrapper}[0], + chain_args => \@cargs, + }); + } + } + } +} + +sub db_access +{ + my $parameters = shift; + my $db_access_mode = $parameters->{db_access_mode} // ""; + my $db_uuid = $parameters->{db_uuid}; + my $sql_query = $parameters->{sql_query}; + + my $access_parameters = { query => $sql_query, uuid => $db_uuid, source => $THIS_FILE, line => __LINE__ }; + + return ($db_access_mode eq "write") + ? { write_code => $anvil->Database->write($access_parameters) } + : $anvil->Database->query($access_parameters); +} + sub foreach_nested { my $parameters = shift; @@ -287,6 +280,134 @@ sub foreach_nested } } +sub get_anvil_data +{ + my $parameters = shift; + my $chain = $parameters->{chain}; + my $target_intermediate = $parameters->{data}; + + my $source_intermediate = $anvil->data; + my $key_index = 0; + + foreach my $key ( @{$chain} ) + { + last if not exists $source_intermediate->{$key}; + + $source_intermediate = $source_intermediate->{$key}; + + if (not exists $target_intermediate->{$key}) + { + $target_intermediate->{$key} = {}; + } + + if ($key_index < $#{$chain}) + { + $target_intermediate = $target_intermediate->{$key}; + } + else + { + $target_intermediate->{$key} = $source_intermediate; + } + + $key_index += 1; + } +} + +sub get_scmd_args +{ + my $parameters = shift; + # Required: + my $input = $parameters->{input}; + my $get_values = $parameters->{get_values}; + # Optional: + my $cmd = $parameters->{cmd}; + my $arg_names = $parameters->{names} // []; + + my $i = 0; + my $args = {}; + my @matches = $get_values->($input, $cmd); + + foreach (@matches) + { + my $arg_name = $arg_names->[$i++] // "$i"; + + $args->{$arg_name} = $_ if defined $arg_name; + } + + return $args; +} + +sub is_array +{ + return ref($_[0]) eq "ARRAY"; +} + +sub is_hash +{ + return ref($_[0]) eq "HASH"; +} + +sub process_scmd_db +{ + my $parameters = shift; + # Required: + my $cmd = $parameters->{cmd}; + my $input = $parameters->{input}; + # Optional: + my $mode = $parameters->{mode}; + + my $sargs = get_scmd_args({ + cmd => $cmd, + input => $input, + get_values => sub { my $c = $_[1]; return $_[0] =~ /^$c\s+(?:uuid=([^\s]+))?\s*(.*)$/; }, + names => ["uuid", "script"], + }); + + eval { + my $results = db_access({ db_uuid => $sargs->{uuid}, sql_query => $sargs->{script}, db_access_mode => $mode }); + + pstdout(JSON->new->utf8->encode($results)); + } or do { + pstderr("failed to access database; cause: $@"); + } +} + +sub process_scmd_execute +{ + my $parameters = shift; + # Required: + my $input = $parameters->{input}; + + my @sargs = parse_line('\s+', 0, $input); + + return if $#sargs < 1; + + my $chain_str = $sargs[1]; + my @chain_args = $#sargs > 1 ? @sargs[2..$#sargs] : (); + + for my $i (0..$#chain_args) + { + my $param = $chain_args[$i]; + my $is_decode_success = eval { $param = decode_json($param); }; + + $chain_args[$i] = $param if $is_decode_success; + } + + my (@results) = access_chain({ chain => $chain_str, chain_args => \@chain_args }); + + pstdout(JSON->new->utf8->allow_blessed->encode({ sub_results => \@results })); +} + +sub pstdout +{ + print $_[0]."\n" if defined $_[0]; +} + +sub pstderr +{ + print STDERR "error: ".$_[0]."\n" if defined $_[0]; +} + $anvil->Get->switches; $anvil->Database->connect; @@ -302,6 +423,7 @@ my $data_hash = $anvil->data->{switches}{'data'}; my $db_access_mode = defined $anvil->data->{switches}{'mode'} ? $anvil->data->{switches}{'mode'} : ""; my $db_uuid = $anvil->data->{switches}{'uuid'}; my $pre_data = $anvil->data->{switches}{'predata'}; +my $script_file = $anvil->data->{switches}{'script'}; my $sql_query = $anvil->data->{switches}{'query'}; my $sub_module_name = defined $anvil->data->{switches}{'sub-module'} ? $anvil->data->{switches}{'sub-module'} : "Database"; my $sub_name = defined $anvil->data->{switches}{'sub'} ? $anvil->data->{switches}{'sub'} : ""; @@ -310,7 +432,8 @@ my $sub_params = defined $anvil->data->{switches}{'sub-params'} ? $anvil->d if ($sql_query) { my $results = db_access({ db_uuid => $db_uuid, sql_query => $sql_query, db_access_mode => $db_access_mode }); - print JSON->new->utf8->encode($results)."\n"; + + pstdout(JSON->new->utf8->encode($results)); } elsif ($anvil->${sub_module_name}->can($sub_name)) { @@ -319,12 +442,14 @@ elsif ($anvil->${sub_module_name}->can($sub_name)) if (not $is_decode_sub_params_success) { - print STDERR "error: failed to parse subroutine parameters\n"; + pstderr("failed to parse subroutine parameters"); + $anvil->nice_exit({ exit_code => 1 }); } my (@results) = $anvil->${sub_module_name}->${sub_name}($decoded_sub_params); - print JSON->new->utf8->encode({ sub_results => scalar(@results) > 1 ? \@results : $results[0] })."\n"; + + pstdout(JSON->new->utf8->encode({ sub_results => scalar(@results) > 1 ? \@results : $results[0] })); } elsif ($data_hash) { @@ -344,7 +469,8 @@ elsif ($data_hash) if (not $is_decode_data_hash_success) { - print STDERR "error: failed to parse data structure\n"; + pstderr("failed to parse data structure"); + $anvil->nice_exit({ exit_code => 1 }); } @@ -355,11 +481,63 @@ elsif ($data_hash) on_chain_end => { fn => \&get_anvil_data, params => $get_anvil_data_params }, }); - print JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})."\n"; + pstdout(JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})); +} +elsif ($script_file) +{ + my $script_file_handle; + + eval { + if ($script_file eq "#!SET!#") + { + $script_file = "-"; + + open($script_file_handle, $script_file); + } + else + { + open($script_file_handle, "< :encoding(UTF-8)", $script_file); + } + } or do { + pstderr("failed to open $script_file as script input; cause: $@"); + + $anvil->nice_exit({ exit_code => 1 }); + }; + + while (my $script_line = <$script_file_handle>) + { + last if ($script_line =~ /^(quit|q)\s+$/); + + $script_line =~ s/\s+$//; + + my $scmd_db_read = "r"; + my $scmd_db_write = "w"; + my $scmd_execute = "x"; + + if ($script_line =~ /^$scmd_db_read\s+/) + { + process_scmd_db({ cmd => $scmd_db_read, input => $script_line }); + } + elsif ($script_line =~ /^$scmd_db_write\s+/) + { + process_scmd_db({ cmd => $scmd_db_write, input => $script_line, mode => "write" }); + } + elsif ($script_line =~ /^$scmd_execute\s+/) + { + process_scmd_execute({ input => $script_line }); + } + } + + close($script_file_handle) or do { + pstderr("failed to close $script_file handle; cause: $@"); + + $anvil->nice_exit({ exit_code => 1 }); + }; } else { - print STDERR "error: missing switches and perhaps their respective parameters; one of --data, --query, or --sub is required\n"; + print STDERR "missing switches/parameters; one of --data, --query, --script, or --sub is required\n"; + $anvil->nice_exit({ exit_code => 1 }); } From b494f79ffe0fb6e23c45672be9d0e9091a8bd39e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 24 Apr 2023 12:54:41 -0400 Subject: [PATCH 46/81] fix(tools): anvil-access-module: default interactive, handle non-existing on class object --- tools/anvil-access-module | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tools/anvil-access-module b/tools/anvil-access-module index f13a0b86..8c80e419 100755 --- a/tools/anvil-access-module +++ b/tools/anvil-access-module @@ -161,12 +161,13 @@ sub access_chain else # Left-hand is not hash; treat it as blessed/class object (module) and try to call a method from it { # Key not found in object; stop following the chain - last if (not defined $intermediate->${key}); + eval { defined $intermediate->${key} ? 1 : 0; } or last; # On the last key of the chain; try to execute the subroutine if it exists if ( $is_last_key && $intermediate->can($key) ) { - eval { (@results) = $intermediate->${key}(@$chain_args); return 1; } or @results = (1); + # Trailing 1 means the eval block will return success if the subroutine and assign succeeded + eval { (@results) = $intermediate->${key}(@$chain_args); 1; } or @results = (1); last; } @@ -420,14 +421,14 @@ if (not $anvil->data->{sys}{database}{connections}) } my $data_hash = $anvil->data->{switches}{'data'}; -my $db_access_mode = defined $anvil->data->{switches}{'mode'} ? $anvil->data->{switches}{'mode'} : ""; +my $db_access_mode = $anvil->data->{switches}{'mode'} // ""; my $db_uuid = $anvil->data->{switches}{'uuid'}; my $pre_data = $anvil->data->{switches}{'predata'}; -my $script_file = $anvil->data->{switches}{'script'}; +my $script_file = $anvil->data->{switches}{'script'} // "-"; my $sql_query = $anvil->data->{switches}{'query'}; -my $sub_module_name = defined $anvil->data->{switches}{'sub-module'} ? $anvil->data->{switches}{'sub-module'} : "Database"; -my $sub_name = defined $anvil->data->{switches}{'sub'} ? $anvil->data->{switches}{'sub'} : ""; -my $sub_params = defined $anvil->data->{switches}{'sub-params'} ? $anvil->data->{switches}{'sub-params'} : "{}"; +my $sub_module_name = $anvil->data->{switches}{'sub-module'} // "Database"; +my $sub_name = $anvil->data->{switches}{'sub'} // ""; +my $sub_params = $anvil->data->{switches}{'sub-params'} // "{}"; if ($sql_query) { @@ -483,12 +484,12 @@ elsif ($data_hash) pstdout(JSON->new->utf8->allow_blessed->encode($get_anvil_data_params->{data})); } -elsif ($script_file) +else { my $script_file_handle; eval { - if ($script_file eq "#!SET!#") + if ($script_file =~ /^#!SET!#|-$/) { $script_file = "-"; @@ -499,7 +500,8 @@ elsif ($script_file) open($script_file_handle, "< :encoding(UTF-8)", $script_file); } } or do { - pstderr("failed to open $script_file as script input; cause: $@"); + # open() sets $! upon error, different from the database module failure (which sets $@) + pstderr("failed to open $script_file as script input; cause: $!"); $anvil->nice_exit({ exit_code => 1 }); }; @@ -529,16 +531,10 @@ elsif ($script_file) } close($script_file_handle) or do { - pstderr("failed to close $script_file handle; cause: $@"); + pstderr("failed to close $script_file handle; cause: $!"); $anvil->nice_exit({ exit_code => 1 }); }; } -else -{ - print STDERR "missing switches/parameters; one of --data, --query, --script, or --sub is required\n"; - - $anvil->nice_exit({ exit_code => 1 }); -} $anvil->nice_exit({ exit_code => 0 }); From fe9c4a758ff0c81df461ebecda4f7881c8a5064b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 24 Apr 2023 15:07:39 -0400 Subject: [PATCH 47/81] docs(tools): explain the interactive/script function of anvil-access-database --- tools/anvil-access-module | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tools/anvil-access-module b/tools/anvil-access-module index 8c80e419..d4b2a23b 100755 --- a/tools/anvil-access-module +++ b/tools/anvil-access-module @@ -15,6 +15,32 @@ # # # --- Usages --- +# To use interactively or process a script: +# anvil-access-module [--script ] +# +# * Inputs are processed by lines. Each line must satisfy one of the +# following format: +# +# r [uuid=] +# +# Performs a data query script (SELECT) on the database. Targets the +# specified database if "uuid=" is provided. +# +# w [uuid=] +# +# Performs a data definition or manipulation script on the database. +# +# x subroutine, or hash available in Anvil::Tools class> [positional subroutine parameters...] +# +# Executes an Anvil module subroutine OR retrieves a hash value. This is +# designed to expose the most-used parts of "$anvil->..." to the +# interactive/script function of this tool. +# +# ! The tool will attempt to decode each positional parameter as JSON. +# Parameters that fail the decoding will be passed to the subroutine +# as-is. +# +# * Lines that fail to meet the format above are ignored. # # To read from database: # anvil-access-module --query [--uuid ] From 8a8b2cbc4b26d1beb64960aaf60f2d70ca6ffd67 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 24 Apr 2023 16:09:36 -0400 Subject: [PATCH 48/81] fix(tools): identify line(s) with UUID in interactive/script anvil-access-module --- tools/anvil-access-module | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tools/anvil-access-module b/tools/anvil-access-module index d4b2a23b..0778caa3 100755 --- a/tools/anvil-access-module +++ b/tools/anvil-access-module @@ -21,25 +21,31 @@ # * Inputs are processed by lines. Each line must satisfy one of the # following format: # -# r [uuid=] +# [ ]r[ uuid=] # # Performs a data query script (SELECT) on the database. Targets the # specified database if "uuid=" is provided. # -# w [uuid=] +# [ ]w[ uuid=] # # Performs a data definition or manipulation script on the database. # -# x subroutine, or hash available in Anvil::Tools class> [positional subroutine parameters...] +# [ ]x subroutine, or hash available in Anvil::Tools class> [space-separated positional subroutine parameters...] # # Executes an Anvil module subroutine OR retrieves a hash value. This is # designed to expose the most-used parts of "$anvil->..." to the # interactive/script function of this tool. # +# * A quoted string is treated as one positional parameter with the +# wrapping quotes removed. +# # ! The tool will attempt to decode each positional parameter as JSON. # Parameters that fail the decoding will be passed to the subroutine # as-is. # +# * The response will be prefixed with line UUID if provided. Line UUID must +# be followed by a space to be recognized. +# # * Lines that fail to meet the format above are ignored. # # To read from database: @@ -381,6 +387,7 @@ sub process_scmd_db my $cmd = $parameters->{cmd}; my $input = $parameters->{input}; # Optional: + my $lid = $parameters->{lid} // ""; my $mode = $parameters->{mode}; my $sargs = get_scmd_args({ @@ -393,7 +400,7 @@ sub process_scmd_db eval { my $results = db_access({ db_uuid => $sargs->{uuid}, sql_query => $sargs->{script}, db_access_mode => $mode }); - pstdout(JSON->new->utf8->encode($results)); + pstdout($lid.JSON->new->utf8->encode($results)); } or do { pstderr("failed to access database; cause: $@"); } @@ -404,6 +411,8 @@ sub process_scmd_execute my $parameters = shift; # Required: my $input = $parameters->{input}; + # Optional: + my $lid = $parameters->{lid} // ""; my @sargs = parse_line('\s+', 0, $input); @@ -422,7 +431,7 @@ sub process_scmd_execute my (@results) = access_chain({ chain => $chain_str, chain_args => \@chain_args }); - pstdout(JSON->new->utf8->allow_blessed->encode({ sub_results => \@results })); + pstdout($lid.JSON->new->utf8->allow_blessed->encode({ sub_results => \@results })); } sub pstdout @@ -534,7 +543,7 @@ else while (my $script_line = <$script_file_handle>) { - last if ($script_line =~ /^(quit|q)\s+$/); + last if ($script_line =~ /^(?:q|quit)\s+$/); $script_line =~ s/\s+$//; @@ -542,17 +551,21 @@ else my $scmd_db_write = "w"; my $scmd_execute = "x"; + $script_line =~ s/^([[:xdigit:]]{8}-[[:xdigit:]]{4}-[1-5][[:xdigit:]]{3}-[89ab][[:xdigit:]]{3}-[[:xdigit:]]{12})\s+//; + + my $script_line_id = $1; + if ($script_line =~ /^$scmd_db_read\s+/) { - process_scmd_db({ cmd => $scmd_db_read, input => $script_line }); + process_scmd_db({ cmd => $scmd_db_read, input => $script_line, lid => $script_line_id }); } elsif ($script_line =~ /^$scmd_db_write\s+/) { - process_scmd_db({ cmd => $scmd_db_write, input => $script_line, mode => "write" }); + process_scmd_db({ cmd => $scmd_db_write, input => $script_line, lid => $script_line_id, mode => "write" }); } elsif ($script_line =~ /^$scmd_execute\s+/) { - process_scmd_execute({ input => $script_line }); + process_scmd_execute({ input => $script_line, lid => $script_line_id }); } } From d1b53b4a2c02839c31837150226c20ae55c4f01f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 24 Apr 2023 23:33:59 -0400 Subject: [PATCH 49/81] fix(striker-ui-api): apply getent to resolve uid:gid --- striker-ui-api/src/lib/consts/PROCESS_OWNER.ts | 7 +++++-- striker-ui-api/src/lib/consts/SERVER_PATHS.ts | 1 + striker-ui-api/src/lib/shell.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts index 6b118789..d37c1c61 100644 --- a/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts +++ b/striker-ui-api/src/lib/consts/PROCESS_OWNER.ts @@ -1,2 +1,5 @@ -export const PUID = process.env.PUID ?? 'striker-ui-api'; -export const PGID = process.env.PGID ?? PUID; +import { resolveGid, resolveUid } from '../shell'; + +export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api'); + +export const PGID = resolveGid(process.env.PGID ?? PUID); diff --git a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts index ed2d7ffe..8192aca6 100644 --- a/striker-ui-api/src/lib/consts/SERVER_PATHS.ts +++ b/striker-ui-api/src/lib/consts/SERVER_PATHS.ts @@ -14,6 +14,7 @@ const EMPTY_SERVER_PATHS: ServerPath = { usr: { bin: { date: {}, + getent: {}, mkfifo: {}, openssl: {}, psql: {}, diff --git a/striker-ui-api/src/lib/shell.ts b/striker-ui-api/src/lib/shell.ts index e3e5a73b..5529bbed 100644 --- a/striker-ui-api/src/lib/shell.ts +++ b/striker-ui-api/src/lib/shell.ts @@ -32,6 +32,9 @@ const systemCall = ( export const date = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.date.self, args); +export const getent = (...args: string[]) => + systemCall(SERVER_PATHS.usr.bin.getent.self, args); + export const mkfifo = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.mkfifo.self, args); @@ -44,6 +47,13 @@ export const rm = (...args: string[]) => export const uuidgen = (...args: string[]) => systemCall(SERVER_PATHS.usr.bin.uuidgen.self, args); +export const resolveId = (id: number | string, database: string) => + Number.parseInt(getent(database, String(id)).split(':', 3)[2]); + +export const resolveGid = (id: number | string) => resolveId(id, 'group'); + +export const resolveUid = (id: number | string) => resolveId(id, 'passwd'); + export const stderr = (message: string) => print(message, { stream: 'stderr' }); export const stdout = (message: string) => print(message); From efc88323213c277c43384afc2ab142a53d2a8ec7 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:30:50 -0400 Subject: [PATCH 50/81] fix(striker-ui-api): add fallback to sanitize --- striker-ui-api/src/lib/sanitize.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/striker-ui-api/src/lib/sanitize.ts b/striker-ui-api/src/lib/sanitize.ts index d76a87a7..24066d91 100644 --- a/striker-ui-api/src/lib/sanitize.ts +++ b/striker-ui-api/src/lib/sanitize.ts @@ -12,6 +12,7 @@ type MapToReturnFunction = { [ReturnTypeName in keyof MapToReturnType]: ( value: unknown, modifier: (unmodified: unknown) => string, + fallback?: MapToReturnType[ReturnTypeName], ) => MapToReturnType[ReturnTypeName]; }; @@ -29,10 +30,10 @@ const MAP_TO_MODIFIER_FUNCTION: MapToModifierFunction = { const MAP_TO_RETURN_FUNCTION: MapToReturnFunction = { boolean: (value) => value !== undefined, - number: (value) => parseFloat(String(value)) || 0, - string: (value, mod) => (value ? mod(value) : ''), - 'string[]': (value, mod) => { - let result: string[] = []; + number: (value, mod, fallback = 0) => parseFloat(String(value)) || fallback, + string: (value, mod, fallback = '') => (value ? mod(value) : fallback), + 'string[]': (value, mod, fallback = []) => { + let result: string[] = fallback; if (value instanceof Array) { result = value.reduce((reduceContainer, element) => { @@ -54,18 +55,24 @@ export const sanitize = ( value: unknown, returnType: ReturnTypeName, { + fallback, modifierType = 'none', modifier = MAP_TO_MODIFIER_FUNCTION[modifierType], }: { + fallback?: MapToReturnType[ReturnTypeName]; modifier?: ModifierFunction; modifierType?: keyof MapToModifierFunction; } = {}, ): MapToReturnType[ReturnTypeName] => - MAP_TO_RETURN_FUNCTION[returnType](value, (unmodified: unknown) => { - const input = String(unmodified); + MAP_TO_RETURN_FUNCTION[returnType]( + value, + (unmodified: unknown) => { + const input = String(unmodified); - return call(modifier, { - notCallableReturn: input, - parameters: [input], - }); - }) as MapToReturnType[ReturnTypeName]; + return call(modifier, { + notCallableReturn: input, + parameters: [input], + }); + }, + fallback, + ) as MapToReturnType[ReturnTypeName]; From ab5ded03cf27293ab8e38002126b4154c28a25cf Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:33:52 -0400 Subject: [PATCH 51/81] fix(striker-ui-api): add label to stdoutVar, simplify uuidgen --- striker-ui-api/src/lib/shell.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/shell.ts b/striker-ui-api/src/lib/shell.ts index 5529bbed..ffff8a7c 100644 --- a/striker-ui-api/src/lib/shell.ts +++ b/striker-ui-api/src/lib/shell.ts @@ -58,5 +58,7 @@ export const stderr = (message: string) => print(message, { stream: 'stderr' }); export const stdout = (message: string) => print(message); -export const stdoutVar = (variable: { [name: string]: unknown }) => - print(`Variables: ${JSON.stringify(variable, null, 2)}`); +export const stdoutVar = (variable: unknown, label = 'Variables: ') => + print(`${label}${JSON.stringify(variable, null, 2)}`); + +export const uuid = () => uuidgen('--random').trim(); From 2812b21f318ea05161ea08f844c39655815583ef Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:35:52 -0400 Subject: [PATCH 52/81] fix(striker-ui-api): export string regex patterns --- .../src/lib/consts/REG_EXP_PATTERNS.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts b/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts index 0a9bd4fe..c62557a6 100644 --- a/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts +++ b/striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts @@ -1,23 +1,21 @@ -const hex = '[0-9a-f]'; -const octet = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; -const alphanumeric = '[a-z0-9]'; -const alphanumericDash = '[a-z0-9-]'; -const ipv4 = `(?:${octet}[.]){3}${octet}`; +export const P_HEX = '[[:xdigit:]]'; +export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; +export const P_ALPHANUM = '[a-z0-9]'; +export const P_ALPHANUM_DASH = '[a-z0-9-]'; +export const P_IPV4 = `(?:${P_OCTET}[.]){3}${P_OCTET}`; +export const P_UUID = `${P_HEX}{8}-${P_HEX}{4}-[1-5]${P_HEX}{3}-[89ab]${P_HEX}{3}-${P_HEX}{12}`; export const REP_DOMAIN = new RegExp( - `^(?:${alphanumeric}(?:${alphanumericDash}{0,61}${alphanumeric})?[.])+${alphanumeric}${alphanumericDash}{0,61}${alphanumeric}$`, + `^(?:${P_ALPHANUM}(?:${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM})?[.])+${P_ALPHANUM}${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM}$`, ); export const REP_INTEGER = /^\d+$/; -export const REP_IPV4 = new RegExp(`^${ipv4}$`); +export const REP_IPV4 = new RegExp(`^${P_IPV4}$`); -export const REP_IPV4_CSV = new RegExp(`(?:${ipv4},)*${ipv4}`); +export const REP_IPV4_CSV = new RegExp(`(?:${P_IPV4},)*${P_IPV4}`); // Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }). export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]+$/; -export const REP_UUID = new RegExp( - `^${hex}{8}-${hex}{4}-[1-5]${hex}{3}-[89ab]${hex}{3}-${hex}{12}$`, - 'i', -); +export const REP_UUID = new RegExp(`^${P_UUID}$`); From 60b2b36e6b623b22e774365ded364aa07fc7b206 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:37:06 -0400 Subject: [PATCH 53/81] fix(striker-ui-api): simplify POST /echo --- striker-ui-api/src/routes/echo.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/routes/echo.ts b/striker-ui-api/src/routes/echo.ts index 929f9880..478dc946 100644 --- a/striker-ui-api/src/routes/echo.ts +++ b/striker-ui-api/src/routes/echo.ts @@ -1,5 +1,7 @@ import express from 'express'; +import { stdoutVar } from '../lib/shell'; + const router = express.Router(); router @@ -7,9 +9,11 @@ router response.status(200).send({ message: 'Empty echo.' }); }) .post('/', (request, response) => { - console.log('echo:post', JSON.stringify(request.body, undefined, 2)); + const { body = {} } = request; + + stdoutVar(body, 'echo:post\n'); - const message = request.body.message ?? 'No message.'; + const { message = 'No message.' } = body; response.status(200).send({ message }); }); From 06ad2a53534ae7e6cc1b0679736cbab0fb4908d9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:41:49 -0400 Subject: [PATCH 54/81] chore(striker-ui-api): add core-js and regenerator-runtime enable promises and async/await --- striker-ui-api/package-lock.json | 33 +++++++++++++++++++++++--------- striker-ui-api/package.json | 4 +++- striker-ui-api/webpack.config.js | 5 ++++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/striker-ui-api/package-lock.json b/striker-ui-api/package-lock.json index 41d491f6..813eabcf 100644 --- a/striker-ui-api/package-lock.json +++ b/striker-ui-api/package-lock.json @@ -8,12 +8,14 @@ "name": "striker-ui-api", "version": "0.1.0", "dependencies": { + "core-js": "^3.30.1", "cors": "^2.8.5", "express": "^4.18.2", "express-session": "^1.17.3", "multer": "^1.4.4", "passport": "^0.6.0", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "regenerator-runtime": "^0.13.11" }, "devDependencies": { "@babel/core": "^7.17.8", @@ -3549,6 +3551,16 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "node_modules/core-js": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", + "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", @@ -5963,10 +5975,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.14.5", @@ -9526,6 +9537,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "core-js": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", + "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==" + }, "core-js-compat": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", @@ -11306,10 +11322,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.14.5", diff --git a/striker-ui-api/package.json b/striker-ui-api/package.json index d40da7ae..980561dd 100644 --- a/striker-ui-api/package.json +++ b/striker-ui-api/package.json @@ -11,12 +11,14 @@ "start": "npm run build && node out/index.js" }, "dependencies": { + "core-js": "^3.30.1", "cors": "^2.8.5", "express": "^4.18.2", "express-session": "^1.17.3", "multer": "^1.4.4", "passport": "^0.6.0", - "passport-local": "^1.0.0" + "passport-local": "^1.0.0", + "regenerator-runtime": "^0.13.11" }, "devDependencies": { "@babel/core": "^7.17.8", diff --git a/striker-ui-api/webpack.config.js b/striker-ui-api/webpack.config.js index 8f5261ab..f866c99e 100644 --- a/striker-ui-api/webpack.config.js +++ b/striker-ui-api/webpack.config.js @@ -11,7 +11,10 @@ module.exports = { use: { loader: 'babel-loader', options: { - presets: ['@babel/preset-env', '@babel/preset-typescript'], + presets: [ + ['@babel/preset-env', { corejs: 3, useBuiltIns: 'usage' }], + '@babel/preset-typescript', + ], }, }, }, From 32ecfec7621f0ae4e4e73b2cf8443e5c195dea56 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:53:27 -0400 Subject: [PATCH 55/81] fix(striker-ui-api): migrate dbQuery->query in access module --- striker-ui-api/src/app.ts | 32 ++-- striker-ui-api/src/index.ts | 28 ++-- striker-ui-api/src/lib/accessModule.ts | 150 ++++++++++++++++-- striker-ui-api/src/lib/getSessionSecret.ts | 8 +- .../buildGetRequestHandler.ts | 51 +++--- .../request_handlers/command/getHostSSH.ts | 93 +++++------ .../manifest/getManifestTemplate.ts | 21 ++- .../request_handlers/server/createServer.ts | 143 +++++++++-------- .../server/getServerDetail.ts | 35 ++-- striker-ui-api/src/routes/file.ts | 98 ++++++------ striker-ui-api/src/types/ApiCommand.d.ts | 15 ++ 11 files changed, 398 insertions(+), 276 deletions(-) create mode 100644 striker-ui-api/src/types/ApiCommand.d.ts diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 32f212b6..c50193ca 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -7,25 +7,27 @@ import routes from './routes'; import { rrouters } from './lib/rrouters'; import session from './session'; -const app = express(); +export default (async () => { + const app = express(); -app.use(json()); + app.use(json()); -app.use(cors()); + app.use(cors()); -// Add session handler to the chain **after** adding other handlers that do -// not depend on session(s). -app.use(session); + // Add session handler to the chain **after** adding other handlers that do + // not depend on session(s). + app.use(await session); -app.use(passport.initialize()); -app.use(passport.authenticate('session')); + app.use(passport.initialize()); + app.use(passport.authenticate('session')); -rrouters(app, routes.private, { - assign: (router) => [guardApi, router], - route: '/api', -}); -rrouters(app, routes.public, { route: '/api' }); + rrouters(app, routes.private, { + assign: (router) => [guardApi, router], + route: '/api', + }); + rrouters(app, routes.public, { route: '/api' }); -app.use(routes.static); + app.use(routes.static); -export default app; + return app; +})(); diff --git a/striker-ui-api/src/index.ts b/striker-ui-api/src/index.ts index 566f927c..8cfe2903 100644 --- a/striker-ui-api/src/index.ts +++ b/striker-ui-api/src/index.ts @@ -5,20 +5,22 @@ import { PGID, PUID, PORT, ECODE_DROP_PRIVILEGES } from './lib/consts'; import app from './app'; import { stderr, stdout } from './lib/shell'; -stdout(`Starting process with ownership ${getuid()}:${getgid()}`); +(async () => { + stdout(`Starting process with ownership ${getuid()}:${getgid()}`); -app.listen(PORT, () => { - try { - // Group must be set before user to avoid permission error. - setgid(PGID); - setuid(PUID); + (await app).listen(PORT, () => { + try { + // Group must be set before user to avoid permission error. + setgid(PGID); + setuid(PUID); - stdout(`Process ownership changed to ${getuid()}:${getgid()}.`); - } catch (error) { - stderr(`Failed to change process ownership; CAUSE: ${error}`); + stdout(`Process ownership changed to ${getuid()}:${getgid()}.`); + } catch (error) { + stderr(`Failed to change process ownership; CAUSE: ${error}`); - process.exit(ECODE_DROP_PRIVILEGES); - } + process.exit(ECODE_DROP_PRIVILEGES); + } - stdout(`Listening on localhost:${PORT}.`); -}); + stdout(`Listening on localhost:${PORT}.`); + }); +})(); diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index ee710981..f33c2cb9 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -1,11 +1,144 @@ -import { spawn, spawnSync, SpawnSyncOptions } from 'child_process'; +import { + ChildProcess, + spawn, + SpawnOptions, + spawnSync, + SpawnSyncOptions, +} from 'child_process'; +import EventEmitter from 'events'; import { readFileSync } from 'fs'; -import { SERVER_PATHS } from './consts'; +import { SERVER_PATHS, PGID, PUID } from './consts'; import { formatSql } from './formatSql'; import { isObject } from './isObject'; -import { date, stderr as sherr, stdout as shout } from './shell'; +import { + date, + stderr as sherr, + stdout as shout, + stdoutVar as shvar, + uuid, +} from './shell'; + +type AccessStartOptions = { + args?: readonly string[]; +} & SpawnOptions; + +class Access extends EventEmitter { + private ps: ChildProcess; + private queue: string[] = []; + + constructor({ + eventEmitterOptions = {}, + spawnOptions = {}, + }: { + eventEmitterOptions?: ConstructorParameters[0]; + spawnOptions?: SpawnOptions; + } = {}) { + super(eventEmitterOptions); + + this.ps = this.start(spawnOptions); + } + + private start({ + args = [], + gid = PGID, + stdio = 'pipe', + timeout = 10000, + uid = PUID, + ...restSpawnOptions + }: AccessStartOptions = {}) { + shvar({ gid, stdio, timeout, uid, ...restSpawnOptions }); + + const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { + gid, + stdio, + timeout, + uid, + ...restSpawnOptions, + }); + + let stderr = ''; + let stdout = ''; + + ps.stderr?.setEncoding('utf-8').on('data', (chunk: string) => { + stderr += chunk; + + const scriptId = this.queue.at(0); + + if (scriptId) { + sherr(`${Access.event(scriptId, 'stderr')}: ${stderr}`); + + stderr = ''; + } + }); + + ps.stdout?.setEncoding('utf-8').on('data', (chunk: string) => { + stdout += chunk; + + let nindex: number = stdout.indexOf('\n'); + + // 1. ~a is the shorthand for -(a + 1) + // 2. negatives are evaluated to true + while (~nindex) { + const scriptId = this.queue.shift(); + + if (scriptId) this.emit(scriptId, stdout.substring(0, nindex)); + + stdout = stdout.substring(nindex + 1); + nindex = stdout.indexOf('\n'); + } + }); + + return ps; + } + + private stop() { + this.ps.once('error', () => !this.ps.killed && this.ps.kill('SIGKILL')); + + this.ps.kill(); + } + + private restart(options?: AccessStartOptions) { + this.ps.once('close', () => this.start(options)); + + this.stop(); + } + + private static event(scriptId: string, category: 'stderr'): string { + return `${scriptId}-${category}`; + } + + public interact(command: string, ...args: string[]) { + const { stdin } = this.ps; + + const scriptId = uuid(); + const script = `${command} ${args.join(' ')}\n`; + + const promise = new Promise((resolve, reject) => { + this.once(scriptId, (data) => { + let result: T; + + try { + result = JSON.parse(data); + } catch (error) { + return reject(`Failed to parse line ${scriptId}; got [${data}]`); + } + + return resolve(result); + }); + }); + + shvar({ scriptId, script }); + + this.queue.push(scriptId); + stdin?.write(script); + + return promise; + } +} + +const access = new Access(); const asyncAnvilAccessModule = ( args: string[], @@ -171,11 +304,8 @@ const dbJobAnvilSyncShared = ( return dbInsertOrUpdateJob(subParams); }; -const dbQuery = (query: string, options?: SpawnSyncOptions) => { - shout(formatSql(query)); - - return execAnvilAccessModule(['--query', query], options); -}; +const query = (sqlscript: string) => + access.interact('r', formatSql(sqlscript)); const dbSubRefreshTimestamp = () => { let result: string; @@ -304,12 +434,12 @@ export { dbInsertOrUpdateJob as job, dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, - dbQuery, dbSubRefreshTimestamp as timestamp, dbWrite, + execModuleSubroutine as sub, getAnvilData, getLocalHostName, getLocalHostUUID, getPeerData, - execModuleSubroutine as sub, + query, }; diff --git a/striker-ui-api/src/lib/getSessionSecret.ts b/striker-ui-api/src/lib/getSessionSecret.ts index f12caa8b..cc20ba04 100644 --- a/striker-ui-api/src/lib/getSessionSecret.ts +++ b/striker-ui-api/src/lib/getSessionSecret.ts @@ -2,18 +2,18 @@ import assert from 'assert'; import { ECODE_SESSION_SECRET, VNAME_SESSION_SECRET } from './consts'; -import { dbQuery, variable } from './accessModule'; +import { query, variable } from './accessModule'; import { openssl, stderr, stdout } from './shell'; -export const getSessionSecret = (): string => { +export const getSessionSecret = async (): Promise => { let sessionSecret: string; try { - const rows: [sessionSecret: string][] = dbQuery( + const rows: [sessionSecret: string][] = await query( `SELECT variable_value FROM variables WHERE variable_name = '${VNAME_SESSION_SECRET}';`, - ).stdout; + ); assert(rows.length > 0, 'No existing session secret found.'); diff --git a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts index 2ddd6e32..051728c3 100644 --- a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts @@ -1,64 +1,53 @@ import { Request, Response } from 'express'; -import { dbQuery } from '../accessModule'; +import { query } from '../accessModule'; import call from '../call'; +import { stderr, stdout, stdoutVar } from '../shell'; const buildGetRequestHandler = ( - query: string | BuildQueryFunction, + sqlscript: string | BuildQueryFunction, { beforeRespond }: BuildGetRequestHandlerOptions = {}, ) => - (request: Request, response: Response) => { - console.log('Calling CLI script to get data.'); + async (request: Request, response: Response) => { + stdout('Calling CLI script to get data.'); const buildQueryOptions: BuildQueryOptions = {}; - let queryStdout; + let result: (number | null | string)[][]; try { - ({ stdout: queryStdout } = dbQuery( - call(query, { + result = await query( + call(sqlscript, { parameters: [request, buildQueryOptions], - notCallableReturn: query, + notCallableReturn: sqlscript, }), - )); + ); } catch (queryError) { - console.log(`Failed to execute query; CAUSE: ${queryError}`); + stderr(`Failed to execute query; CAUSE: ${queryError}`); response.status(500).send(); return; } - console.log( - `Query stdout pre-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, - ); + stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `); const { afterQueryReturn } = buildQueryOptions; - queryStdout = call(afterQueryReturn, { - parameters: [queryStdout], - notCallableReturn: queryStdout, + result = call(afterQueryReturn, { + parameters: [result], + notCallableReturn: result, }); - queryStdout = call(beforeRespond, { - parameters: [queryStdout], - notCallableReturn: queryStdout, + result = call(beforeRespond, { + parameters: [result], + notCallableReturn: result, }); - console.log( - `Query stdout post-hooks (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, - ); + stdoutVar(result, `Query stdout post-hooks (type=[${typeof result}]): `); - response.json(queryStdout); + response.json(result); }; export default buildGetRequestHandler; diff --git a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts index 258d9f49..7bf007bb 100644 --- a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts +++ b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts @@ -1,71 +1,68 @@ +import assert from 'assert'; import { RequestHandler } from 'express'; +import { REP_IPV4, REP_PEACEFUL_STRING } from '../../consts'; import { HOST_KEY_CHANGED_PREFIX } from '../../consts/HOST_KEY_CHANGED_PREFIX'; -import { dbQuery, getLocalHostUUID, getPeerData } from '../../accessModule'; -import { sanitizeSQLParam } from '../../sanitizeSQLParam'; +import { getLocalHostUUID, getPeerData, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; import { stderr } from '../../shell'; export const getHostSSH: RequestHandler< unknown, - { - badSSHKeys?: DeleteSshKeyConflictRequestBody; - hostName: string; - hostOS: string; - hostUUID: string; - isConnected: boolean; - isInetConnected: boolean; - isOSRegistered: boolean; - }, - { - password: string; - port?: number; - ipAddress: string; - } -> = (request, response) => { + GetHostSshResponseBody, + GetHostSshRequestBody +> = async (request, response) => { const { - body: { password, port = 22, ipAddress: target }, + body: { password: rpassword, port: rport = 22, ipAddress: rtarget }, } = request; - let hostName: string; - let hostOS: string; - let hostUUID: string; - let isConnected: boolean; - let isInetConnected: boolean; - let isOSRegistered: boolean; + const password = sanitize(rpassword, 'string'); + const port = sanitize(rport, 'number'); + const target = sanitize(rtarget, 'string', { modifierType: 'sql' }); + + try { + assert( + REP_PEACEFUL_STRING.test(password), + `Password must be a peaceful string; got [${password}]`, + ); + + assert( + Number.isInteger(port), + `Port must be a valid integer; got [${port}]`, + ); + + assert( + REP_IPV4.test(target), + `IP address must be a valid IPv4 address; got [${target}]`, + ); + } catch (assertError) { + stderr(`Assert failed when getting host SSH data; CAUSE: ${assertError}`); + + return response.status(400).send(); + } const localHostUUID = getLocalHostUUID(); + let rsbody: GetHostSshResponseBody; + try { - ({ - hostName, - hostOS, - hostUUID, - isConnected, - isInetConnected, - isOSRegistered, - } = getPeerData(target, { password, port })); + rsbody = getPeerData(target, { password, port }); } catch (subError) { stderr(`Failed to get peer data; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - let badSSHKeys: DeleteSshKeyConflictRequestBody | undefined; - - if (!isConnected) { - const rows = dbQuery(` + if (!rsbody.isConnected) { + const rows: [stateNote: string, stateUUID: string][] = await query(` SELECT sta.state_note, sta.state_uuid FROM states AS sta WHERE sta.state_host_uuid = '${localHostUUID}' - AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${sanitizeSQLParam( - target, - )}';`).stdout as [stateNote: string, stateUUID: string][]; + AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${target}';`); if (rows.length > 0) { - badSSHKeys = rows.reduce( + rsbody.badSSHKeys = rows.reduce( (previous, [, stateUUID]) => { previous[localHostUUID].push(stateUUID); @@ -76,13 +73,5 @@ export const getHostSSH: RequestHandler< } } - response.status(200).send({ - badSSHKeys, - hostName, - hostOS, - hostUUID, - isConnected, - isInetConnected, - isOSRegistered, - }); + response.status(200).send(rsbody); }; diff --git a/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts b/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts index d3a87085..8a02b345 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts @@ -1,6 +1,6 @@ import { RequestHandler } from 'express'; -import { dbQuery, getLocalHostName } from '../../accessModule'; +import { getLocalHostName, query } from '../../accessModule'; import { getHostNameDomain, getHostNamePrefix, @@ -8,17 +8,18 @@ import { } from '../../disassembleHostName'; import { stderr } from '../../shell'; -export const getManifestTemplate: RequestHandler = (request, response) => { - let localHostName = ''; +export const getManifestTemplate: RequestHandler = async ( + request, + response, +) => { + let localHostName: string; try { localHostName = getLocalHostName(); } catch (subError) { stderr(String(subError)); - response.status(500).send(); - - return; + return response.status(500).send(); } const localShortHostName = getShortHostName(localHostName); @@ -38,7 +39,7 @@ export const getManifestTemplate: RequestHandler = (request, response) => { >; try { - ({ stdout: rawQueryResult } = dbQuery( + rawQueryResult = await query( `SELECT a.fence_uuid, a.fence_name, @@ -71,13 +72,11 @@ export const getManifestTemplate: RequestHandler = (request, response) => { ORDER BY manifest_name DESC LIMIT 1 ) AS c ON a.row_number = c.row_number;`, - )); + ); } catch (queryError) { stderr(`Failed to execute query; CAUSE: ${queryError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } const queryResult = rawQueryResult.reduce< diff --git a/striker-ui-api/src/lib/request_handlers/server/createServer.ts b/striker-ui-api/src/lib/request_handlers/server/createServer.ts index cae0d884..a5b48c0a 100644 --- a/striker-ui-api/src/lib/request_handlers/server/createServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/createServer.ts @@ -1,107 +1,120 @@ import assert from 'assert'; import { RequestHandler } from 'express'; +import { REP_UUID, SERVER_PATHS } from '../../consts'; import { OS_LIST_MAP } from '../../consts/OS_LIST'; -import { REP_INTEGER, REP_UUID } from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; -import { dbQuery, job } from '../../accessModule'; -import { stderr, stdout } from '../../shell'; +import { job, query } from '../../accessModule'; +import { sanitize } from '../../sanitize'; +import { stderr, stdout, stdoutVar } from '../../shell'; -export const createServer: RequestHandler = ({ body }, response) => { - stdout(`Creating server.\n${JSON.stringify(body, null, 2)}`); +export const createServer: RequestHandler = async (request, response) => { + const { body: rqbody = {} } = request; + + stdoutVar({ rqbody }, 'Creating server.\n'); const { - serverName, - cpuCores, - memory, + serverName: rServerName, + cpuCores: rCpuCores, + memory: rMemory, virtualDisks: [ - { storageSize = undefined, storageGroupUUID = undefined } = {}, + { + storageSize: rStorageSize = undefined, + storageGroupUUID: rStorageGroupUuid = undefined, + } = {}, ] = [], - installISOFileUUID, - driverISOFileUUID, - anvilUUID, - optimizeForOS, - } = body || {}; - - const dataServerName = String(serverName); - const dataOS = String(optimizeForOS); - const dataCPUCores = String(cpuCores); - const dataRAM = String(memory); - const dataStorageGroupUUID = String(storageGroupUUID); - const dataStorageSize = String(storageSize); - const dataInstallISO = String(installISOFileUUID); - const dataDriverISO = String(driverISOFileUUID) || 'none'; - const dataAnvilUUID = String(anvilUUID); + installISOFileUUID: rInstallIsoUuid, + driverISOFileUUID: rDriverIsoUuid, + anvilUUID: rAnvilUuid, + optimizeForOS: rOptimizeForOs, + } = rqbody; + + const serverName = sanitize(rServerName, 'string'); + const os = sanitize(rOptimizeForOs, 'string'); + const cpuCores = sanitize(rCpuCores, 'number'); + const memory = sanitize(rMemory, 'number'); + const storageGroupUUID = sanitize(rStorageGroupUuid, 'string'); + const storageSize = sanitize(rStorageSize, 'number'); + const installIsoUuid = sanitize(rInstallIsoUuid, 'string'); + const driverIsoUuid = sanitize(rDriverIsoUuid, 'string', { + fallback: 'none', + }); + const anvilUuid = sanitize(rAnvilUuid, 'string'); try { assert( - /^[0-9a-z_-]+$/i.test(dataServerName), - `Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${dataServerName}].`, + /^[0-9a-z_-]+$/i.test(serverName), + `Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${serverName}]`, ); - const [[serverNameCount]] = dbQuery( - `SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${dataServerName}'`, - ).stdout; + const [[serverNameCount]] = await query( + `SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${serverName}'`, + ); assert( serverNameCount === 0, - `Data server name already exists; got [${dataServerName}]`, + `Data server name already exists; got [${serverName}]`, ); + assert( - OS_LIST_MAP[dataOS] !== undefined, - `Data OS not recognized; got [${dataOS}].`, + OS_LIST_MAP[os] !== undefined, + `Data OS not recognized; got [${os}]`, ); + assert( - REP_INTEGER.test(dataCPUCores), - `Data CPU cores can only contain digits; got [${dataCPUCores}].`, + Number.isInteger(cpuCores), + `Data CPU cores can only contain digits; got [${cpuCores}]`, ); + assert( - REP_INTEGER.test(dataRAM), - `Data RAM can only contain digits; got [${dataRAM}].`, + Number.isInteger(memory), + `Data RAM can only contain digits; got [${memory}]`, ); + assert( - REP_UUID.test(dataStorageGroupUUID), - `Data storage group UUID must be a valid UUID; got [${dataStorageGroupUUID}].`, + REP_UUID.test(storageGroupUUID), + `Data storage group UUID must be a valid UUID; got [${storageGroupUUID}]`, ); + assert( - REP_INTEGER.test(dataStorageSize), - `Data storage size can only contain digits; got [${dataStorageSize}].`, + Number.isInteger(storageSize), + `Data storage size can only contain digits; got [${storageSize}]`, ); + assert( - REP_UUID.test(dataInstallISO), - `Data install ISO must be a valid UUID; got [${dataInstallISO}].`, + REP_UUID.test(installIsoUuid), + `Data install ISO must be a valid UUID; got [${installIsoUuid}]`, ); + assert( - dataDriverISO === 'none' || REP_UUID.test(dataDriverISO), - `Data driver ISO must be a valid UUID when provided; got [${dataDriverISO}].`, + driverIsoUuid === 'none' || REP_UUID.test(driverIsoUuid), + `Data driver ISO must be a valid UUID when provided; got [${driverIsoUuid}]`, ); + assert( - REP_UUID.test(dataAnvilUUID), - `Data anvil UUID must be a valid UUID; got [${dataAnvilUUID}].`, + REP_UUID.test(anvilUuid), + `Data anvil UUID must be a valid UUID; got [${anvilUuid}]`, ); } catch (assertError) { stdout( - `Failed to assert value when trying to provision a server; CAUSE: ${assertError}.`, + `Failed to assert value when trying to provision a server; CAUSE: ${assertError}`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } - const provisionServerJobData = `server_name=${dataServerName} -os=${dataOS} -cpu_cores=${dataCPUCores} -ram=${dataRAM} -storage_group_uuid=${dataStorageGroupUUID} -storage_size=${dataStorageSize} -install_iso=${dataInstallISO} -driver_iso=${dataDriverISO}`; + const provisionServerJobData = `server_name=${serverName} +os=${os} +cpu_cores=${cpuCores} +ram=${memory} +storage_group_uuid=${storageGroupUUID} +storage_size=${storageSize} +install_iso=${installIsoUuid} +driver_iso=${driverIsoUuid}`; stdout(`provisionServerJobData=[${provisionServerJobData}]`); - const [[provisionServerJobHostUUID]] = dbQuery( + const [[provisionServerJobHostUUID]]: [[string]] = await query( `SELECT CASE WHEN pri_hos.primary_host_uuid IS NULL @@ -120,7 +133,7 @@ driver_iso=${dataDriverISO}`; AND sca_clu_nod.scan_cluster_node_crmd_member AND sca_clu_nod.scan_cluster_node_cluster_member AND (NOT sca_clu_nod.scan_cluster_node_maintenance_mode) - AND anv.anvil_uuid = '${dataAnvilUUID}' + AND anv.anvil_uuid = '${anvilUuid}' ORDER BY sca_clu_nod.scan_cluster_node_name LIMIT 1 ) AS pri_hos @@ -129,10 +142,10 @@ driver_iso=${dataDriverISO}`; 1 AS phr, anv.anvil_node1_host_uuid AS node1_host_uuid FROM anvils AS anv - WHERE anv.anvil_uuid = '${dataAnvilUUID}' + WHERE anv.anvil_uuid = '${anvilUuid}' ) AS nod_1 ON pri_hos.phl = nod_1.phr;`, - ).stdout; + ); stdout(`provisionServerJobHostUUID=[${provisionServerJobHostUUID}]`); @@ -149,9 +162,7 @@ driver_iso=${dataDriverISO}`; } catch (subError) { stderr(`Failed to provision server; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(202).send(); diff --git a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts index c847133e..a9641700 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts @@ -3,10 +3,9 @@ import { RequestHandler } from 'express'; import { createReadStream } from 'fs'; import path from 'path'; -import { REP_UUID } from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; +import { REP_UUID, SERVER_PATHS } from '../../consts'; -import { dbQuery, getLocalHostUUID, job } from '../../accessModule'; +import { getLocalHostUUID, job, query } from '../../accessModule'; import { sanitize } from '../../sanitize'; import { mkfifo, rm, stderr, stdout } from '../../shell'; @@ -18,7 +17,7 @@ const rmfifo = (path: string) => { } }; -export const getServerDetail: RequestHandler = (request, response) => { +export const getServerDetail: RequestHandler = async (request, response) => { const { serverUUID } = request.params; const { ss, resize } = request.query; @@ -39,9 +38,7 @@ export const getServerDetail: RequestHandler = (request, response) => { `Failed to assert value when trying to get server detail; CAUSE: ${assertError}.`, ); - response.status(500).send(); - - return; + return response.status(500).send(); } if (isScreenshot) { @@ -52,24 +49,20 @@ export const getServerDetail: RequestHandler = (request, response) => { } catch (subError) { stderr(String(subError)); - response.status(500).send(); - - return; + return response.status(500).send(); } stdout(`requestHostUUID=[${requestHostUUID}]`); try { - [[serverHostUUID]] = dbQuery(` + [[serverHostUUID]] = await query(` SELECT server_host_uuid FROM servers - WHERE server_uuid = '${serverUUID}';`).stdout; + WHERE server_uuid = '${serverUUID}';`); } catch (queryError) { stderr(`Failed to get server host UUID; CAUSE: ${queryError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdout(`serverHostUUID=[${serverHostUUID}]`); @@ -94,9 +87,9 @@ export const getServerDetail: RequestHandler = (request, response) => { namedPipeReadStream.once('close', () => { stdout(`On close; removing named pipe at ${imageFilePath}.`); - response.status(200).send({ screenshot: imageData }); - rmfifo(imageFilePath); + + return response.status(200).send({ screenshot: imageData }); }); namedPipeReadStream.on('data', (data) => { @@ -123,11 +116,9 @@ export const getServerDetail: RequestHandler = (request, response) => { `Failed to prepare named pipe and/or receive image data; CAUSE: ${prepPipeError}`, ); - response.status(500).send(); - rmfifo(imageFilePath); - return; + return response.status(500).send(); } let resizeArgs = sanitize(resize, 'string'); @@ -152,9 +143,7 @@ out-file-id=${epoch}`, } catch (subError) { stderr(`Failed to queue fetch server screenshot job; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } } else { // For getting sever detail data. diff --git a/striker-ui-api/src/routes/file.ts b/striker-ui-api/src/routes/file.ts index bfcb63ff..6c348001 100644 --- a/striker-ui-api/src/routes/file.ts +++ b/striker-ui-api/src/routes/file.ts @@ -1,31 +1,33 @@ import express from 'express'; +import { DELETED } from '../lib/consts'; + import { dbJobAnvilSyncShared, - dbQuery, timestamp, dbWrite, + query, } from '../lib/accessModule'; import getFile from '../lib/request_handlers/file/getFile'; import getFileDetail from '../lib/request_handlers/file/getFileDetail'; import uploadSharedFiles from '../middlewares/uploadSharedFiles'; +import { stderr, stdout, stdoutVar } from '../lib/shell'; const router = express.Router(); router - .delete('/:fileUUID', (request, response) => { + .delete('/:fileUUID', async (request, response) => { const { fileUUID } = request.params; - const FILE_TYPE_DELETED = 'DELETED'; - const [[oldFileType]] = dbQuery( + const [[oldFileType]] = await query( `SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, - ).stdout; + ); - if (oldFileType !== FILE_TYPE_DELETED) { + if (oldFileType !== DELETED) { dbWrite( `UPDATE files SET - file_type = '${FILE_TYPE_DELETED}', + file_type = '${DELETED}', modified_date = '${timestamp()}' WHERE file_uuid = '${fileUUID}';`, ).stdout; @@ -40,11 +42,10 @@ router .get('/', getFile) .get('/:fileUUID', getFileDetail) .post('/', uploadSharedFiles.single('file'), ({ file, body }, response) => { - console.log('Receiving shared file.'); + stdout('Receiving shared file.'); if (file) { - console.log(`file: ${JSON.stringify(file, null, 2)}`); - console.log(`body: ${JSON.stringify(body, null, 2)}`); + stdoutVar({ body, file }); dbJobAnvilSyncShared( 'move_incoming', @@ -56,24 +57,26 @@ router response.status(200).send(); } }) - .put('/:fileUUID', (request, response) => { - console.log('Begin edit single file.'); - console.dir(request.body); + .put('/:fileUUID', async (request, response) => { + const { body = {}, params } = request; - const { fileUUID } = request.params; - const { fileName, fileLocations, fileType } = request.body; + stdoutVar(body, 'Begin edit single file. body='); + + const { fileUUID } = params; + const { fileName, fileLocations, fileType } = body; const anvilSyncSharedFunctions = []; - let query = ''; + let sqlscript = ''; if (fileName) { - const [[oldFileName]] = dbQuery( + const [[oldFileName]] = await query( `SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, - ).stdout; - console.log(`oldFileName=[${oldFileName}],newFileName=[${fileName}]`); + ); + + stdoutVar({ oldFileName, fileName }); if (fileName !== oldFileName) { - query += ` + sqlscript += ` UPDATE files SET file_name = '${fileName}', @@ -93,7 +96,7 @@ router } if (fileType) { - query += ` + sqlscript += ` UPDATE files SET file_type = '${fileType}', @@ -113,7 +116,7 @@ router if (fileLocations) { fileLocations.forEach( - ({ + async ({ fileLocationUUID, isFileLocationActive, }: { @@ -132,14 +135,18 @@ router jobDescription = '0133'; } - query += ` - UPDATE file_locations - SET - file_location_active = '${fileLocationActive}', - modified_date = '${timestamp()}' - WHERE file_location_uuid = '${fileLocationUUID}';`; - - const targetHosts = dbQuery( + sqlscript += ` + UPDATE file_locations + SET + file_location_active = '${fileLocationActive}', + modified_date = '${timestamp()}' + WHERE file_location_uuid = '${fileLocationUUID}';`; + + const targetHosts: [ + n1uuid: string, + n2uuid: string, + dr1uuid: null | string, + ][] = await query( `SELECT anv.anvil_node1_host_uuid, anv.anvil_node2_host_uuid, @@ -148,9 +155,9 @@ router JOIN file_locations AS fil_loc ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`, - ).stdout; + ); - targetHosts.flat().forEach((hostUUID: string) => { + targetHosts.flat().forEach((hostUUID: null | string) => { if (hostUUID) { anvilSyncSharedFunctions.push(() => dbJobAnvilSyncShared( @@ -167,34 +174,23 @@ router ); } - console.log(`Query (type=[${typeof query}]): [${query}]`); + stdout(`Query (type=[${typeof sqlscript}]): [${sqlscript}]`); let queryStdout; try { - ({ stdout: queryStdout } = dbWrite(query)); + ({ stdout: queryStdout } = dbWrite(sqlscript)); } catch (queryError) { - console.log(`Failed to execute query; CAUSE: ${queryError}`); + stderr(`Failed to execute query; CAUSE: ${queryError}`); - response.status(500).send(); + return response.status(500).send(); } - console.log( - `Query stdout (type=[${typeof queryStdout}]): ${JSON.stringify( - queryStdout, - null, - 2, - )}`, + stdoutVar(queryStdout, `Query stdout (type=[${typeof queryStdout}]): `); + + anvilSyncSharedFunctions.forEach((fn, index) => + stdoutVar(fn(), `Anvil sync shared [${index}] output: `), ); - anvilSyncSharedFunctions.forEach((fn, index) => { - console.log( - `Anvil sync shared [${index}] output: [${JSON.stringify( - fn(), - null, - 2, - )}]`, - ); - }); response.status(200).send(queryStdout); }); diff --git a/striker-ui-api/src/types/ApiCommand.d.ts b/striker-ui-api/src/types/ApiCommand.d.ts new file mode 100644 index 00000000..d3d03adf --- /dev/null +++ b/striker-ui-api/src/types/ApiCommand.d.ts @@ -0,0 +1,15 @@ +type GetHostSshRequestBody = { + password: string; + port?: number; + ipAddress: string; +}; + +type GetHostSshResponseBody = { + badSSHKeys?: DeleteSshKeyConflictRequestBody; + hostName: string; + hostOS: string; + hostUUID: string; + isConnected: boolean; + isInetConnected: boolean; + isOSRegistered: boolean; +}; From c4232916f92de8295984eede63ce7a609902c46c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 22:56:00 -0400 Subject: [PATCH 56/81] fix(striker-ui-api): remove console calls --- .../anvil/buildQueryAnvilDetail.ts | 3 ++- .../file/buildQueryFileDetail.ts | 3 ++- .../lib/request_handlers/host/configStriker.ts | 16 ++++++---------- .../src/lib/request_handlers/server/getServer.ts | 3 ++- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts b/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts index 227d0291..a1561bd5 100644 --- a/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts @@ -2,6 +2,7 @@ import NODE_AND_DR_RESERVED_MEMORY_SIZE from '../../consts/NODE_AND_DR_RESERVED_ import { OS_LIST } from '../../consts/OS_LIST'; import join from '../../join'; +import { stdoutVar } from '../../shell'; const buildQueryAnvilDetail = ({ anvilUUIDs = ['*'], @@ -19,7 +20,7 @@ const buildQueryAnvilDetail = ({ separator: ', ', }); - console.log(`condAnvilsUUID=[${condAnvilsUUID}]`); + stdoutVar({ condAnvilsUUID }); const buildHostQuery = ({ isSummary = false, diff --git a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts index 7bb2ba50..2c36e0bf 100644 --- a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts @@ -1,4 +1,5 @@ import join from '../../join'; +import { stdoutVar } from '../../shell'; const buildQueryFileDetail = ({ fileUUIDs = ['*'], @@ -14,7 +15,7 @@ const buildQueryFileDetail = ({ separator: ', ', }); - console.log(`condFilesUUID=[${condFileUUIDs}]`); + stdoutVar({ condFileUUIDs }); return ` SELECT diff --git a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts index 0ae24596..3e7aee72 100644 --- a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts +++ b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts @@ -10,6 +10,7 @@ import { import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; +import { stderr, stdoutVar } from '../../shell'; const fvar = (configStepCount: number, fieldName: string) => ['form', `config_step${configStepCount}`, fieldName, 'value'].join('::'); @@ -40,8 +41,7 @@ export const configStriker: RequestHandler< undefined, InitializeStrikerForm > = ({ body }, response) => { - console.log('Begin initialize Striker.'); - console.dir(body, { depth: null }); + stdoutVar(body, 'Begin initialize Striker; body='); const { adminPassword = '', @@ -105,13 +105,11 @@ export const configStriker: RequestHandler< `Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${dataOrganizationPrefix}]`, ); } catch (assertError) { - console.log( + stderr( `Failed to assert value when trying to initialize striker; CAUSE: ${assertError}.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { @@ -153,11 +151,9 @@ ${buildNetworkLinks(2, networkShortName, interfaces)}`; job_description: 'job_0071', }); } catch (subError) { - console.log(`Failed to queue striker initialization; CAUSE: ${subError}`); - - response.status(500).send(); + stderr(`Failed to queue striker initialization; CAUSE: ${subError}`); - return; + return response.status(500).send(); } response.status(200).send(); diff --git a/striker-ui-api/src/lib/request_handlers/server/getServer.ts b/striker-ui-api/src/lib/request_handlers/server/getServer.ts index 5aebe5b3..516e73ed 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServer.ts @@ -1,6 +1,7 @@ import buildGetRequestHandler from '../buildGetRequestHandler'; import join from '../../join'; import { sanitize } from '../../sanitize'; +import { stdoutVar } from '../../shell'; export const getServer = buildGetRequestHandler( (request, buildQueryOptions) => { @@ -13,7 +14,7 @@ export const getServer = buildGetRequestHandler( separator: ', ', }); - console.log(`condAnvilsUUID=[${condAnvilUUIDs}]`); + stdoutVar({ condAnvilUUIDs }); if (buildQueryOptions) { buildQueryOptions.afterQueryReturn = (queryStdout) => { From dc4a49a94c2acde31faacc2d21133807e1a66a9b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 25 Apr 2023 23:22:01 -0400 Subject: [PATCH 57/81] fix(striker-ui-api): sanitize config striker input, rename rqbody->body --- .../request_handlers/host/configStriker.ts | 83 ++++++++++--------- .../request_handlers/server/createServer.ts | 6 +- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts index 3e7aee72..fc68cac1 100644 --- a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts +++ b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts @@ -3,13 +3,14 @@ import { RequestHandler } from 'express'; import { REP_DOMAIN, - REP_INTEGER, REP_IPV4, REP_IPV4_CSV, -} from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; + REP_PEACEFUL_STRING, + SERVER_PATHS, +} from '../../consts'; import { job } from '../../accessModule'; +import { sanitize } from '../../sanitize'; import { stderr, stdoutVar } from '../../shell'; const fvar = (configStepCount: number, fieldName: string) => @@ -39,70 +40,72 @@ ${fvar( export const configStriker: RequestHandler< unknown, undefined, - InitializeStrikerForm -> = ({ body }, response) => { + Partial +> = (request, response) => { + const { body = {} } = request; + stdoutVar(body, 'Begin initialize Striker; body='); const { - adminPassword = '', - domainName = '', - hostName = '', - hostNumber = 0, - networkDNS = '', - networkGateway = '', + adminPassword: rAdminPassword = '', + domainName: rDomainName = '', + hostName: rHostName = '', + hostNumber: rHostNumber = 0, + networkDNS: rNetworkDns = '', + networkGateway: rNetworkGateway = '', networks = [], - organizationName = '', - organizationPrefix = '', - } = body || {}; - - const dataAdminPassword = String(adminPassword); - const dataDomainName = String(domainName); - const dataHostName = String(hostName); - const dataHostNumber = String(hostNumber); - const dataNetworkDNS = String(networkDNS); - const dataNetworkGateway = String(networkGateway); - const dataOrganizationName = String(organizationName); - const dataOrganizationPrefix = String(organizationPrefix); + organizationName: rOrganizationName = '', + organizationPrefix: rOrganizationPrefix = '', + } = body; + + const adminPassword = sanitize(rAdminPassword, 'string'); + const domainName = sanitize(rDomainName, 'string'); + const hostName = sanitize(rHostName, 'string'); + const hostNumber = sanitize(rHostNumber, 'number'); + const networkDns = sanitize(rNetworkDns, 'string'); + const networkGateway = sanitize(rNetworkGateway, 'string'); + const organizationName = sanitize(rOrganizationName, 'string'); + const organizationPrefix = sanitize(rOrganizationPrefix, 'string'); try { assert( - !/['"/\\><}{]/g.test(dataAdminPassword), - `Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${dataAdminPassword}]`, + REP_PEACEFUL_STRING.test(adminPassword), + `Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${adminPassword}]`, ); assert( - REP_DOMAIN.test(dataDomainName), - `Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${dataDomainName}]`, + REP_DOMAIN.test(domainName), + `Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${domainName}]`, ); assert( - REP_DOMAIN.test(dataHostName), - `Data host name can only contain alphanumeric, hyphen, and dot characters; got [${dataHostName}]`, + REP_DOMAIN.test(hostName), + `Data host name can only contain alphanumeric, hyphen, and dot characters; got [${hostName}]`, ); assert( - REP_INTEGER.test(dataHostNumber) && hostNumber > 0, - `Data host number can only contain digits; got [${dataHostNumber}]`, + Number.isInteger(hostNumber) && hostNumber > 0, + `Data host number can only contain digits; got [${hostNumber}]`, ); assert( - REP_IPV4_CSV.test(dataNetworkDNS), - `Data network DNS must be a comma separated list of valid IPv4 addresses; got [${dataNetworkDNS}]`, + REP_IPV4_CSV.test(networkDns), + `Data network DNS must be a comma separated list of valid IPv4 addresses; got [${networkDns}]`, ); assert( - REP_IPV4.test(dataNetworkGateway), - `Data network gateway must be a valid IPv4 address; got [${dataNetworkGateway}]`, + REP_IPV4.test(networkGateway), + `Data network gateway must be a valid IPv4 address; got [${networkGateway}]`, ); assert( - dataOrganizationName.length > 0, - `Data organization name cannot be empty; got [${dataOrganizationName}]`, + REP_PEACEFUL_STRING.test(organizationName), + `Data organization name cannot be empty; got [${organizationName}]`, ); assert( - /^[a-z0-9]{1,5}$/.test(dataOrganizationPrefix), - `Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${dataOrganizationPrefix}]`, + /^[a-z0-9]{1,5}$/.test(organizationPrefix), + `Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${organizationPrefix}]`, ); } catch (assertError) { stderr( @@ -120,7 +123,7 @@ export const configStriker: RequestHandler< ${fvar(1, 'organization')}=${organizationName} ${fvar(1, 'prefix')}=${organizationPrefix} ${fvar(1, 'sequence')}=${hostNumber} -${fvar(2, 'dns')}=${networkDNS} +${fvar(2, 'dns')}=${networkDns} ${fvar(2, 'gateway')}=${networkGateway} ${fvar(2, 'host_name')}=${hostName} ${fvar(2, 'striker_password')}=${adminPassword} diff --git a/striker-ui-api/src/lib/request_handlers/server/createServer.ts b/striker-ui-api/src/lib/request_handlers/server/createServer.ts index a5b48c0a..5c1cdc39 100644 --- a/striker-ui-api/src/lib/request_handlers/server/createServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/createServer.ts @@ -9,9 +9,9 @@ import { sanitize } from '../../sanitize'; import { stderr, stdout, stdoutVar } from '../../shell'; export const createServer: RequestHandler = async (request, response) => { - const { body: rqbody = {} } = request; + const { body = {} } = request; - stdoutVar({ rqbody }, 'Creating server.\n'); + stdoutVar(body, 'Creating server; body='); const { serverName: rServerName, @@ -27,7 +27,7 @@ export const createServer: RequestHandler = async (request, response) => { driverISOFileUUID: rDriverIsoUuid, anvilUUID: rAnvilUuid, optimizeForOS: rOptimizeForOs, - } = rqbody; + } = body; const serverName = sanitize(rServerName, 'string'); const os = sanitize(rOptimizeForOs, 'string'); From da785218ec3a1d1e442814cd362e280be3384570 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 00:09:11 -0400 Subject: [PATCH 58/81] fix(striker-ui-api): migrate a/dbWrite to daemon write --- striker-ui-api/src/lib/accessModule.ts | 96 ++---------- .../lib/request_handlers/user/deleteUser.ts | 20 +-- striker-ui-api/src/passport.ts | 18 ++- striker-ui-api/src/routes/file.ts | 16 +- striker-ui-api/src/session.ts | 141 +++++++----------- 5 files changed, 89 insertions(+), 202 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index f33c2cb9..93e5cc40 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -11,7 +11,6 @@ import { readFileSync } from 'fs'; import { SERVER_PATHS, PGID, PUID } from './consts'; import { formatSql } from './formatSql'; -import { isObject } from './isObject'; import { date, stderr as sherr, @@ -140,52 +139,16 @@ class Access extends EventEmitter { const access = new Access(); -const asyncAnvilAccessModule = ( - args: string[], - { - onClose, - onError, - stdio = 'pipe', - timeout = 60000, - ...restSpawnOptions - }: AsyncAnvilAccessModuleOptions = {}, -) => { - const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { - stdio, - timeout, - ...restSpawnOptions, - }); - - let stderr = ''; - let stdout = ''; - - ps.once('close', (ecode, signal) => { - let output; - - try { - output = JSON.parse(stdout); - } catch (stdoutParseError) { - output = stdout; - - sherr( - `Failed to parse async anvil-access-module stdout; CAUSE: ${stdoutParseError}`, - ); - } - - onClose?.call(null, { ecode, signal, stderr, stdout: output }); - }); - - ps.once('error', (...args) => { - onError?.call(null, ...args); - }); - - ps.stderr?.setEncoding('utf-8').on('data', (data) => { - stderr += data; - }); - - ps.stdout?.setEncoding('utf-8').on('data', (data) => { - stdout += data; - }); +const query = (script: string) => + access.interact('r', formatSql(script)); + +const write = async (script: string) => { + const { write_code: wcode } = await access.interact<{ write_code: number }>( + 'w', + formatSql(script), + ); + + return wcode; }; const execAnvilAccessModule = ( @@ -304,9 +267,6 @@ const dbJobAnvilSyncShared = ( return dbInsertOrUpdateJob(subParams); }; -const query = (sqlscript: string) => - access.interact('r', formatSql(sqlscript)); - const dbSubRefreshTimestamp = () => { let result: string; @@ -321,39 +281,6 @@ const dbSubRefreshTimestamp = () => { return result; }; -const awrite = ( - script: string, - { onClose: initOnClose, ...restOptions }: AsyncDatabaseWriteOptions = {}, -) => { - shout(formatSql(script)); - - const onClose: AsyncAnvilAccessModuleCloseHandler = (args, ...rest) => { - const { stdout } = args; - const { obj: output } = isObject(stdout); - - let wcode: number | null = null; - - if ('write_code' in output) { - ({ write_code: wcode } = output as { write_code: number }); - - shout(`Async write completed with write_code=${wcode}`); - } - - initOnClose?.call(null, { wcode, ...args }, ...rest); - }; - - return asyncAnvilAccessModule(['--query', script, '--mode', 'write'], { - onClose, - ...restOptions, - }); -}; - -const dbWrite = (script: string, options?: SpawnSyncOptions) => { - shout(formatSql(script)); - - return execAnvilAccessModule(['--query', script, '--mode', 'write'], options); -}; - const getAnvilData = ( dataStruct: AnvilDataStruct, { predata, ...spawnSyncOptions }: GetAnvilDataOptions = {}, @@ -430,16 +357,15 @@ const getPeerData: GetPeerDataFunction = ( }; export { - awrite, dbInsertOrUpdateJob as job, dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, dbSubRefreshTimestamp as timestamp, - dbWrite, execModuleSubroutine as sub, getAnvilData, getLocalHostName, getLocalHostUUID, getPeerData, query, + write, }; diff --git a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts index 64008967..2170ba3c 100644 --- a/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts +++ b/striker-ui-api/src/lib/request_handlers/user/deleteUser.ts @@ -3,7 +3,7 @@ import { RequestHandler } from 'express'; import { DELETED, REP_UUID } from '../../consts'; -import { awrite } from '../../accessModule'; +import { write } from '../../accessModule'; import join from '../../join'; import { sanitize } from '../../sanitize'; import { stderr, stdoutVar } from '../../shell'; @@ -12,7 +12,7 @@ export const deleteUser: RequestHandler< DeleteUserParamsDictionary, undefined, DeleteUserRequestBody -> = (request, response) => { +> = async (request, response) => { const { body: { uuids: rawUserUuidList } = {}, params: { userUuid }, @@ -42,23 +42,13 @@ export const deleteUser: RequestHandler< } try { - awrite( + const wcode = await write( `UPDATE users SET user_algorithm = '${DELETED}' WHERE user_uuid IN (${join(ulist)});`, - { - onClose: ({ ecode, wcode }) => { - if (ecode !== 0 || wcode !== 0) { - stderr( - `SQL script failed in delete user(s); ecode=${ecode}, wcode=${wcode}`, - ); - } - }, - onError: (error) => { - stderr(`Delete user subprocess error; CAUSE: ${error}`); - }, - }, ); + + assert(wcode === 0, `Write exited with code ${wcode}`); } catch (error) { stderr(`Failed to delete user(s); CAUSE: ${error}`); diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts index 1ebcdbf0..48d7f97e 100644 --- a/striker-ui-api/src/passport.ts +++ b/striker-ui-api/src/passport.ts @@ -1,13 +1,15 @@ import passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; -import { dbQuery, sub } from './lib/accessModule'; +import { DELETED } from './lib/consts'; + +import { query, sub } from './lib/accessModule'; import { sanitize } from './lib/sanitize'; import { stdout } from './lib/shell'; passport.use( 'login', - new LocalStrategy((username, password, done) => { + new LocalStrategy(async (username, password, done) => { stdout(`Attempting passport local strategy "login" for user [${username}]`); let rows: [ @@ -20,7 +22,7 @@ passport.use( ][]; try { - rows = dbQuery( + rows = await query( `SELECT user_uuid, user_name, @@ -32,7 +34,7 @@ passport.use( WHERE user_algorithm != 'DELETED' AND user_name = '${username}' LIMIT 1;`, - ).stdout; + ); } catch (queryError) { return done(queryError); } @@ -89,7 +91,7 @@ passport.serializeUser((user, done) => { return done(null, uuid); }); -passport.deserializeUser((id, done) => { +passport.deserializeUser(async (id, done) => { const uuid = sanitize(id, 'string', { modifierType: 'sql' }); stdout(`Deserialize user identified by ${uuid}`); @@ -97,12 +99,12 @@ passport.deserializeUser((id, done) => { let rows: [userName: string][]; try { - rows = dbQuery( + rows = await query( `SELECT user_name FROM users - WHERE user_algorithm != 'DELETED' + WHERE user_algorithm != '${DELETED}' AND user_uuid = '${uuid}';`, - ).stdout; + ); } catch (error) { return done(error); } diff --git a/striker-ui-api/src/routes/file.ts b/striker-ui-api/src/routes/file.ts index 6c348001..cc64a7d3 100644 --- a/striker-ui-api/src/routes/file.ts +++ b/striker-ui-api/src/routes/file.ts @@ -5,8 +5,8 @@ import { DELETED } from '../lib/consts'; import { dbJobAnvilSyncShared, timestamp, - dbWrite, query, + write, } from '../lib/accessModule'; import getFile from '../lib/request_handlers/file/getFile'; import getFileDetail from '../lib/request_handlers/file/getFileDetail'; @@ -24,13 +24,13 @@ router ); if (oldFileType !== DELETED) { - dbWrite( + await write( `UPDATE files SET file_type = '${DELETED}', modified_date = '${timestamp()}' WHERE file_uuid = '${fileUUID}';`, - ).stdout; + ); dbJobAnvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { jobHostUUID: 'all', @@ -174,25 +174,21 @@ router ); } - stdout(`Query (type=[${typeof sqlscript}]): [${sqlscript}]`); - - let queryStdout; + let wcode: number; try { - ({ stdout: queryStdout } = dbWrite(sqlscript)); + wcode = await write(sqlscript); } catch (queryError) { stderr(`Failed to execute query; CAUSE: ${queryError}`); return response.status(500).send(); } - stdoutVar(queryStdout, `Query stdout (type=[${typeof queryStdout}]): `); - anvilSyncSharedFunctions.forEach((fn, index) => stdoutVar(fn(), `Anvil sync shared [${index}] output: `), ); - response.status(200).send(queryStdout); + response.status(200).send(wcode); }); export default router; diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index ce769bda..510f9137 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -1,16 +1,14 @@ +import assert from 'assert'; import expressSession, { SessionData, Store as BaseSessionStore, } from 'express-session'; -import { - awrite, - dbQuery, - getLocalHostUUID, - timestamp, -} from './lib/accessModule'; +import { DELETED } from './lib/consts'; + +import { getLocalHostUUID, query, timestamp, write } from './lib/accessModule'; import { getSessionSecret } from './lib/getSessionSecret'; -import { stderr, stdout, stdoutVar, uuidgen } from './lib/shell'; +import { stderr, stdout, stdoutVar, uuid } from './lib/shell'; const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 3600000; @@ -19,38 +17,33 @@ export class SessionStore extends BaseSessionStore { super(options); } - public destroy( + public async destroy( sid: string, done?: ((err?: unknown) => void) | undefined, - ): void { + ): Promise { stdout(`Destroy session ${sid}`); try { - awrite(`DELETE FROM sessions WHERE session_uuid = '${sid}';`, { - onClose({ wcode }) { - if (wcode !== 0) { - stderr( - `SQL script failed during destroy session ${sid}; code: ${wcode}`, - ); - } - }, - onError(error) { - stderr( - `Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`, - ); - }, - }); + const wcode = await write( + `UPDATE sessions SET session_salt = '${DELETED}' WHERE session_uuid = '${sid}';`, + ); + + assert(wcode === 0, `Write exited with code ${wcode}`); } catch (error) { + stderr( + `Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`, + ); + return done?.call(null, error); } return done?.call(null); } - public get( + public async get( sid: string, done: (err: unknown, session?: SessionData | null | undefined) => void, - ): void { + ): Promise { stdout(`Get session ${sid}`); let rows: [ @@ -60,7 +53,7 @@ export class SessionStore extends BaseSessionStore { ][]; try { - rows = dbQuery( + rows = await query( `SELECT s.session_uuid, u.user_uuid, @@ -69,7 +62,7 @@ export class SessionStore extends BaseSessionStore { JOIN users AS u ON s.session_user_uuid = u.user_uuid WHERE s.session_uuid = '${sid}';`, - ).stdout; + ); } catch (queryError) { return done(queryError); } @@ -90,20 +83,18 @@ export class SessionStore extends BaseSessionStore { maxAge: cookieMaxAge, originalMaxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, }, - passport: { - user: userUuid, - }, + passport: { user: userUuid }, }; return done(null, data); } - public set( + public async set( sid: string, session: SessionData, done?: ((err?: unknown) => void) | undefined, - ): void { - stdout(`Set session ${sid}`); + ): Promise { + stdoutVar({ session }, `Set session ${sid}`); const { passport: { user: userUuid }, @@ -113,7 +104,7 @@ export class SessionStore extends BaseSessionStore { const localHostUuid = getLocalHostUUID(); const modifiedDate = timestamp(); - awrite( + const wcode = await write( `INSERT INTO sessions ( session_uuid, @@ -131,56 +122,39 @@ export class SessionStore extends BaseSessionStore { '${modifiedDate}' ) ON CONFLICT (session_uuid) - DO UPDATE SET session_host_uuid = '${localHostUuid}', - modified_date = '${modifiedDate}';`, - { - onClose: ({ wcode }) => { - if (wcode !== 0) { - stderr( - `SQL script failed during set session ${sid}; code: ${wcode}`, - ); - } - }, - onError: (error) => { - stderr( - `Failed to complete DB write in set session ${sid}; CAUSE: ${error}`, - ); - }, - }, + DO UPDATE SET modified_date = '${modifiedDate}';`, ); + + assert(wcode === 0, `Write exited with code ${wcode}`); } catch (error) { + stderr( + `Failed to complete DB write in set session ${sid}; CAUSE: ${error}`, + ); + return done?.call(null, error); } return done?.call(null); } - public touch( + public async touch( sid: string, session: SessionData, done?: ((err?: unknown) => void) | undefined, - ): void { - stdout(`Touch session ${sid}`); + ): Promise { + stdoutVar({ session }, `Touch session ${sid}`); try { - awrite( + const wcode = await write( `UPDATE sessions SET modified_date = '${timestamp()}' WHERE session_uuid = '${sid}';`, - { - onClose: ({ wcode }) => { - if (wcode !== 0) { - stderr( - `SQL script failed during touch session ${sid}; code: ${wcode}`, - ); - } - }, - onError: (error) => { - stderr( - `Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`, - ); - }, - }, ); + + assert(wcode === 0, `Write exited with code ${wcode}`); } catch (error) { + stderr( + `Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`, + ); + return done?.call(null, error); } @@ -201,19 +175,18 @@ export class SessionStore extends BaseSessionStore { } } -const session = expressSession({ - cookie: { maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE }, - genid: ({ path }) => { - const sid = uuidgen('--random').trim(); - - stdout(`Generated session identifier ${sid}; request.path=${path}`); - - return sid; - }, - resave: false, - saveUninitialized: false, - secret: getSessionSecret(), - store: new SessionStore(), -}); - -export default session; +export default (async () => + expressSession({ + cookie: { maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE }, + genid: ({ path }) => { + const sid = uuid(); + + stdout(`Generated session identifier ${sid}; request.path=${path}`); + + return sid; + }, + resave: false, + saveUninitialized: false, + secret: await getSessionSecret(), + store: new SessionStore(), + }))(); From 3efc5d8bfad443525ca575ed9cc4db0b01c10b37 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 12:08:54 -0400 Subject: [PATCH 59/81] fix(striker-ui-api): mark session as deleted on destroy --- striker-ui-api/src/session.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index 510f9137..87476ffb 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -25,7 +25,9 @@ export class SessionStore extends BaseSessionStore { try { const wcode = await write( - `UPDATE sessions SET session_salt = '${DELETED}' WHERE session_uuid = '${sid}';`, + `UPDATE sessions + SET session_salt = '${DELETED}', modified_date = '${timestamp()}' + WHERE session_uuid = '${sid}';`, ); assert(wcode === 0, `Write exited with code ${wcode}`); @@ -61,7 +63,8 @@ export class SessionStore extends BaseSessionStore { FROM sessions AS s JOIN users AS u ON s.session_user_uuid = u.user_uuid - WHERE s.session_uuid = '${sid}';`, + WHERE s.session_salt != '${DELETED}' + AND s.session_uuid = '${sid}';`, ); } catch (queryError) { return done(queryError); @@ -146,7 +149,9 @@ export class SessionStore extends BaseSessionStore { try { const wcode = await write( - `UPDATE sessions SET modified_date = '${timestamp()}' WHERE session_uuid = '${sid}';`, + `UPDATE sessions + SET modified_date = '${timestamp()}' + WHERE session_uuid = '${sid}';`, ); assert(wcode === 0, `Write exited with code ${wcode}`); From ea78090ac00c5e5cffb17ec62e1d4c97e6ff7f04 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 12:10:50 -0400 Subject: [PATCH 60/81] fix(striker-ui-api): remove not-implemented .at() --- striker-ui-api/src/lib/accessModule.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 93e5cc40..17af7d9c 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -63,7 +63,7 @@ class Access extends EventEmitter { ps.stderr?.setEncoding('utf-8').on('data', (chunk: string) => { stderr += chunk; - const scriptId = this.queue.at(0); + const scriptId: string | undefined = this.queue[0]; if (scriptId) { sherr(`${Access.event(scriptId, 'stderr')}: ${stderr}`); From cb6a699fc779631f14788420dc78127bb61f45d3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 15:35:51 -0400 Subject: [PATCH 61/81] fix(striker-ui-api): connect data, subroutine wrappers to access interact --- striker-ui-api/src/lib/accessModule.ts | 34 +++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 17af7d9c..389ab5f2 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -128,7 +128,7 @@ class Access extends EventEmitter { }); }); - shvar({ scriptId, script }); + shvar({ scriptId, script }, 'Access interact: '); this.queue.push(scriptId); stdin?.write(script); @@ -139,6 +139,36 @@ class Access extends EventEmitter { const access = new Access(); +const data = async (...keys: string[]) => + access.interact('x', `data->${keys.join('->')}`); + +const subroutine = >( + subroutine: string, + { + params = [], + pre = ['Database'], + }: { + params?: unknown[]; + pre?: string[]; + }, +) => { + const chain = `${pre.join('->')}->${subroutine}`; + + const subParams: string[] = params.map((p) => { + let result: string; + + try { + result = JSON.stringify(p); + } catch (error) { + result = String(p); + } + + return result; + }); + + return access.interact('x', chain, ...subParams); +}; + const query = (script: string) => access.interact('r', formatSql(script)); @@ -366,6 +396,8 @@ export { getLocalHostName, getLocalHostUUID, getPeerData, + data, query, + subroutine, write, }; From 1e84641157927aaf0d6c85332dc7fa502ca18897 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 16:41:22 -0400 Subject: [PATCH 62/81] fix(striker-ui-api): migrate getPeerData to use access interact --- striker-ui-api/src/lib/accessModule.ts | 30 ++++++++----- .../request_handlers/command/getHostSSH.ts | 10 ++--- .../host/createHostConnection.ts | 43 ++++++------------- .../src/types/GetPeerDataFunction.d.ts | 16 +++---- 4 files changed, 45 insertions(+), 54 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 389ab5f2..0259e19d 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -142,7 +142,7 @@ const access = new Access(); const data = async (...keys: string[]) => access.interact('x', `data->${keys.join('->')}`); -const subroutine = >( +const subroutine = async ( subroutine: string, { params = [], @@ -163,10 +163,18 @@ const subroutine = >( result = String(p); } - return result; + return `'${result}'`; }); - return access.interact('x', chain, ...subParams); + const { sub_results: results } = await access.interact<{ sub_results: T }>( + 'x', + chain, + ...subParams, + ); + + shvar(results, `${chain} results: `); + + return results; }; const query = (script: string) => @@ -357,9 +365,9 @@ const getLocalHostUUID = () => { return result; }; -const getPeerData: GetPeerDataFunction = ( +const getPeerData: GetPeerDataFunction = async ( target, - { password, port, ...restOptions } = {}, + { password, port } = {}, ) => { const [ rawIsConnected, @@ -370,11 +378,13 @@ const getPeerData: GetPeerDataFunction = ( internet: rawIsInetConnected, os_registered: rawIsOSRegistered, }, - ] = execModuleSubroutine('get_peer_data', { - subModuleName: 'Striker', - subParams: { password, port, target }, - ...restOptions, - }).stdout as [connected: string, data: PeerDataHash]; + ]: [connected: string, data: PeerDataHash] = await subroutine( + 'get_peer_data', + { + params: [{ password, port, target }], + pre: ['Striker'], + }, + ); return { hostName, diff --git a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts index 7bf007bb..c800dcd2 100644 --- a/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts +++ b/striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts @@ -14,12 +14,12 @@ export const getHostSSH: RequestHandler< GetHostSshRequestBody > = async (request, response) => { const { - body: { password: rpassword, port: rport = 22, ipAddress: rtarget }, + body: { password: rPassword, port: rPort = 22, ipAddress: rTarget } = {}, } = request; - const password = sanitize(rpassword, 'string'); - const port = sanitize(rport, 'number'); - const target = sanitize(rtarget, 'string', { modifierType: 'sql' }); + const password = sanitize(rPassword, 'string'); + const port = sanitize(rPort, 'number'); + const target = sanitize(rTarget, 'string', { modifierType: 'sql' }); try { assert( @@ -47,7 +47,7 @@ export const getHostSSH: RequestHandler< let rsbody: GetHostSshResponseBody; try { - rsbody = getPeerData(target, { password, port }); + rsbody = await getPeerData(target, { password, port }); } catch (subError) { stderr(`Failed to get peer data; CAUSE: ${subError}`); diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index 38a66989..f4cfc5ca 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -16,7 +16,7 @@ export const createHostConnection: RequestHandler< unknown, undefined, CreateHostConnectionRequestBody -> = (request, response) => { +> = async (request, response) => { const { body: { dbName = 'anvil', @@ -46,16 +46,15 @@ export const createHostConnection: RequestHandler< let peerHostUUID: string; try { - ({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = getPeerData( - peerIPAddress, - { password: commonPassword, port: peerSSHPort }, - )); + ({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = + await getPeerData(peerIPAddress, { + password: commonPassword, + port: peerSSHPort, + })); } catch (subError) { stderr(`Failed to get peer data; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ peerHostUUID, isPeerReachable }); @@ -65,9 +64,7 @@ export const createHostConnection: RequestHandler< `Cannot connect to peer; please verify credentials and SSH keys validity.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { @@ -78,9 +75,7 @@ export const createHostConnection: RequestHandler< } catch (subError) { stderr(`Failed to get matching IP address; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ localIPAddress }); @@ -107,9 +102,7 @@ export const createHostConnection: RequestHandler< } catch (subError) { stderr(`Failed to write ${pgpassFilePath}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } try { @@ -130,9 +123,7 @@ export const createHostConnection: RequestHandler< } catch (fsError) { stderr(`Failed to remove ${pgpassFilePath}; CAUSE: ${fsError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } stdoutVar({ isPeerDBReachable }); @@ -142,9 +133,7 @@ export const createHostConnection: RequestHandler< `Cannot connect to peer database; please verify database credentials.`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } const localHostUUID = getLocalHostUUID(); @@ -160,9 +149,7 @@ export const createHostConnection: RequestHandler< } catch (subError) { stderr(`Failed to get local database data from hash; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } const jobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${peerHostUUID} --host ${peerIPAddress} --port ${commonDBPort} --ping ${commonPing}`; @@ -181,9 +168,7 @@ peer_job_command=${peerJobCommand}`, } catch (subError) { stderr(`Failed to add peer ${peerHostUUID}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(201).send(); diff --git a/striker-ui-api/src/types/GetPeerDataFunction.d.ts b/striker-ui-api/src/types/GetPeerDataFunction.d.ts index 17258540..62a32791 100644 --- a/striker-ui-api/src/types/GetPeerDataFunction.d.ts +++ b/striker-ui-api/src/types/GetPeerDataFunction.d.ts @@ -6,23 +6,19 @@ type PeerDataHash = { os_registered: string; }; -type GetPeerDataOptions = Omit< - ExecModuleSubroutineOptions, - 'subModuleName' | 'subParams' -> & - ModuleSubroutineCommonParams & { - password?: string; - port?: number; - }; +type GetPeerDataOptions = ModuleSubroutineCommonParams & { + password?: string; + port?: number; +}; type GetPeerDataFunction = ( target: string, options?: GetPeerDataOptions, -) => { +) => Promise<{ hostName: string; hostOS: string; hostUUID: string; isConnected: boolean; isInetConnected: boolean; isOSRegistered: boolean; -}; +}>; From bc42b40a5af477ace01d18bb61a83311b5dc2aac Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 26 Apr 2023 16:44:47 -0400 Subject: [PATCH 63/81] fix(striker-ui-api): set cookie max age to 8 hours --- striker-ui-api/src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index 87476ffb..1d9b1133 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -10,7 +10,7 @@ import { getLocalHostUUID, query, timestamp, write } from './lib/accessModule'; import { getSessionSecret } from './lib/getSessionSecret'; import { stderr, stdout, stdoutVar, uuid } from './lib/shell'; -const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 3600000; +const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours export class SessionStore extends BaseSessionStore { constructor(options = {}) { From f075e219dedf168c25d6e91738c1ba78789ae60b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 00:04:59 -0400 Subject: [PATCH 64/81] feat(striker-ui-api): add initial traverse function --- striker-ui-api/src/lib/traverse.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 striker-ui-api/src/lib/traverse.ts diff --git a/striker-ui-api/src/lib/traverse.ts b/striker-ui-api/src/lib/traverse.ts new file mode 100644 index 00000000..994fefb1 --- /dev/null +++ b/striker-ui-api/src/lib/traverse.ts @@ -0,0 +1,28 @@ +type NestedObject = { + [key: number | string]: NestedObject | T; +}; + +export const traverse = , V = unknown>( + obj: O, + init: T, + onKey: (previous: T, obj: O, key: string) => { is: boolean; next: O }, + { + onEnd, + previous = init, + }: { + onEnd?: (previous: T, obj: O, key: string) => void; + previous?: T; + } = {}, +) => { + Object.keys(obj).forEach((key: string) => { + const { is: proceed, next } = onKey(previous, obj, key); + + if (proceed) { + traverse(next, init, onKey, { previous }); + } else { + onEnd?.call(null, previous, obj, key); + } + }); + + return previous; +}; From 66b02d4a7784ad3ba0530b4c1ed2036b19c0dc3f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 00:56:07 -0400 Subject: [PATCH 65/81] fix(striker-ui-api): allow async callback in GET request builder --- .../lib/request_handlers/buildGetRequestHandler.ts | 14 +++++++------- striker-ui-api/src/types/BuildQueryFunction.d.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts index 051728c3..fcb65376 100644 --- a/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts @@ -6,7 +6,7 @@ import { stderr, stdout, stdoutVar } from '../shell'; const buildGetRequestHandler = ( - sqlscript: string | BuildQueryFunction, + scriptOrCallback: string | BuildQueryFunction, { beforeRespond }: BuildGetRequestHandlerOptions = {}, ) => async (request: Request, response: Response) => { @@ -17,12 +17,12 @@ const buildGetRequestHandler = let result: (number | null | string)[][]; try { - result = await query( - call(sqlscript, { - parameters: [request, buildQueryOptions], - notCallableReturn: sqlscript, - }), - ); + const sqlscript: string = + typeof scriptOrCallback === 'function' + ? await scriptOrCallback(request, buildQueryOptions) + : scriptOrCallback; + + result = await query(sqlscript); } catch (queryError) { stderr(`Failed to execute query; CAUSE: ${queryError}`); diff --git a/striker-ui-api/src/types/BuildQueryFunction.d.ts b/striker-ui-api/src/types/BuildQueryFunction.d.ts index 78077027..ead3207d 100644 --- a/striker-ui-api/src/types/BuildQueryFunction.d.ts +++ b/striker-ui-api/src/types/BuildQueryFunction.d.ts @@ -7,4 +7,4 @@ type BuildQueryOptions = { type BuildQueryFunction = ( request: import('express').Request, options?: BuildQueryOptions, -) => string; +) => string | Promise; From f9d7449b7def33bece91be3d214def8911d6555b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 00:57:58 -0400 Subject: [PATCH 66/81] fix(striker-ui-api): migrate get anvil data to access interact --- striker-ui-api/src/lib/accessModule.ts | 67 +++++++++++++------ .../request_handlers/command/runManifest.ts | 36 +++------- .../fence/getFenceTemplate.ts | 10 ++- .../host/createHostConnection.ts | 8 +-- .../host/getHostConnection.ts | 8 +-- .../manifest/getManifestDetail.ts | 19 ++---- .../request_handlers/ups/getUPSTemplate.ts | 9 +-- .../src/types/GetAnvilDataFunction.d.ts | 2 +- 8 files changed, 77 insertions(+), 82 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 0259e19d..6a92d3be 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -139,9 +139,6 @@ class Access extends EventEmitter { const access = new Access(); -const data = async (...keys: string[]) => - access.interact('x', `data->${keys.join('->')}`); - const subroutine = async ( subroutine: string, { @@ -150,7 +147,7 @@ const subroutine = async ( }: { params?: unknown[]; pre?: string[]; - }, + } = {}, ) => { const chain = `${pre.join('->')}->${subroutine}`; @@ -319,19 +316,29 @@ const dbSubRefreshTimestamp = () => { return result; }; -const getAnvilData = ( - dataStruct: AnvilDataStruct, - { predata, ...spawnSyncOptions }: GetAnvilDataOptions = {}, -): HashType => - execAnvilAccessModule( - [ - '--predata', - JSON.stringify(predata), - '--data', - JSON.stringify(dataStruct), - ], - spawnSyncOptions, - ).stdout; +const getData = async (...keys: string[]) => { + const chain = `data->${keys.join('->')}`; + + const { + sub_results: [data], + } = await access.interact<{ sub_results: [T] }>('x', chain); + + shvar(data, `${chain} data: `); + + return data; +}; + +const getFenceSpec = async () => { + await subroutine('get_fence_data', { pre: ['Striker'] }); + + return getData('fence_data'); +}; + +const getHostData = async () => { + await subroutine('get_hosts'); + + return getData('hosts'); +}; const getLocalHostName = () => { let result: string; @@ -349,7 +356,7 @@ const getLocalHostName = () => { return result; }; -const getLocalHostUUID = () => { +const getLocalHostUuid = () => { let result: string; try { @@ -365,6 +372,15 @@ const getLocalHostUUID = () => { return result; }; +const getManifestData = async (manifestUuid?: string) => { + await subroutine('load_manifest', { + params: [{ manifest_uuid: manifestUuid }], + pre: ['Striker'], + }); + + return getData('manifests'); +}; + const getPeerData: GetPeerDataFunction = async ( target, { password, port } = {}, @@ -396,17 +412,26 @@ const getPeerData: GetPeerDataFunction = async ( }; }; +const getUpsSpec = async () => { + await subroutine('get_ups_data', { pre: ['Striker'] }); + + return getData('ups_data'); +}; + export { dbInsertOrUpdateJob as job, dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, dbSubRefreshTimestamp as timestamp, execModuleSubroutine as sub, - getAnvilData, + getData, + getFenceSpec, + getHostData, getLocalHostName, - getLocalHostUUID, + getLocalHostUuid as getLocalHostUUID, + getManifestData, getPeerData, - data, + getUpsSpec, query, subroutine, write, diff --git a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts index 6f98296a..d3a6537c 100644 --- a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts @@ -4,7 +4,13 @@ import { RequestHandler } from 'express'; import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts/REG_EXP_PATTERNS'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; -import { getAnvilData, job, sub } from '../../accessModule'; +import { + getData, + getHostData, + getManifestData, + job, + sub, +} from '../../accessModule'; import { sanitize } from '../../sanitize'; import { stderr } from '../../shell'; @@ -12,7 +18,7 @@ export const runManifest: RequestHandler< { manifestUuid: string }, undefined, RunManifestRequestBody -> = (request, response) => { +> = async (request, response) => { const { params: { manifestUuid }, body: { @@ -85,29 +91,9 @@ export const runManifest: RequestHandler< let rawSysData: AnvilDataSysHash | undefined; try { - ({ - hosts: rawHostListData, - manifests: rawManifestListData, - sys: rawSysData, - } = getAnvilData<{ - hosts?: AnvilDataHostListHash; - manifests?: AnvilDataManifestListHash; - sys?: AnvilDataSysHash; - }>( - { hosts: true, manifests: true, sys: true }, - { - predata: [ - ['Database->get_hosts'], - [ - 'Striker->load_manifest', - { - debug, - manifest_uuid: manifestUuid, - }, - ], - ], - }, - )); + rawHostListData = await getHostData(); + rawManifestListData = await getManifestData(manifestUuid); + rawSysData = await getData('sys'); } catch (subError) { stderr( `Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, diff --git a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts index beec0a0e..7a723fcd 100644 --- a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts @@ -1,15 +1,13 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; + +import { getFenceSpec } from '../../accessModule'; import { stderr } from '../../shell'; -export const getFenceTemplate: RequestHandler = (request, response) => { +export const getFenceTemplate: RequestHandler = async (request, response) => { let rawFenceData; try { - ({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>( - { fence_data: true }, - { predata: [['Striker->get_fence_data']] }, - )); + rawFenceData = await getFenceSpec(); } catch (subError) { stderr(`Failed to get fence device template; CAUSE: ${subError}`); diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index f4cfc5ca..4fe3b1a5 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -3,7 +3,7 @@ import { RequestHandler } from 'express'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { - getAnvilData, + getData, getLocalHostUUID, getPeerData, job, @@ -140,10 +140,8 @@ export const createHostConnection: RequestHandler< try { const { - database: { - [localHostUUID]: { port: rawLocalDBPort }, - }, - } = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true }); + [localHostUUID]: { port: rawLocalDBPort }, + } = await getData('database'); localDBPort = sanitize(rawLocalDBPort, 'number'); } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index 0bf8da69..0f06c9d5 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -1,4 +1,4 @@ -import { getAnvilData, getLocalHostUUID } from '../../accessModule'; +import { getData, getLocalHostUUID } from '../../accessModule'; import { buildUnknownIDCondition } from '../../buildCondition'; import buildGetRequestHandler from '../buildGetRequestHandler'; import { toLocal } from '../../convertHostUUID'; @@ -39,7 +39,7 @@ const buildHostConnections = ( ); export const getHostConnection = buildGetRequestHandler( - (request, buildQueryOptions) => { + async (request, buildQueryOptions) => { const { hostUUIDs: rawHostUUIDs } = request.query; let rawDatabaseData: AnvilDataDatabaseHash; @@ -59,9 +59,7 @@ export const getHostConnection = buildGetRequestHandler( stdout(`condHostUUIDs=[${condHostUUIDs}]`); try { - ({ database: rawDatabaseData } = getAnvilData<{ - database: AnvilDataDatabaseHash; - }>({ database: true })); + rawDatabaseData = await getData('database'); } catch (subError) { throw new Error(`Failed to get anvil data; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts b/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts index 0da929a9..fcabc918 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts @@ -1,6 +1,6 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; +import { getManifestData } from '../../accessModule'; import { getEntityParts } from '../../disassembleEntityId'; import { stderr, stdout } from '../../shell'; @@ -67,25 +67,18 @@ const handleSortNetworks = ( return result; }; -export const getManifestDetail: RequestHandler = (request, response) => { +export const getManifestDetail: RequestHandler = async (request, response) => { const { - params: { manifestUUID }, + params: { manifestUUID: manifestUuid }, } = request; let rawManifestListData: AnvilDataManifestListHash | undefined; try { - ({ manifests: rawManifestListData } = getAnvilData<{ - manifests?: AnvilDataManifestListHash; - }>( - { manifests: true }, - { - predata: [['Striker->load_manifest', { manifest_uuid: manifestUUID }]], - }, - )); + rawManifestListData = await getManifestData(manifestUuid); } catch (subError) { stderr( - `Failed to get install manifest ${manifestUUID}; CAUSE: ${subError}`, + `Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, ); response.status(500).send(); @@ -109,7 +102,7 @@ export const getManifestDetail: RequestHandler = (request, response) => { const { manifest_uuid: { - [manifestUUID]: { + [manifestUuid]: { parsed: { domain, fences: fenceUuidList = {}, diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index 395e46b7..3c3f4f76 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -1,16 +1,13 @@ import { RequestHandler } from 'express'; -import { getAnvilData } from '../../accessModule'; +import { getUpsSpec } from '../../accessModule'; import { stderr } from '../../shell'; -export const getUPSTemplate: RequestHandler = (request, response) => { +export const getUPSTemplate: RequestHandler = async (request, response) => { let rawUPSData: AnvilDataUPSHash; try { - ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( - { ups_data: true }, - { predata: [['Striker->get_ups_data']] }, - )); + rawUPSData = await getUpsSpec(); } catch (subError) { stderr(`Failed to get ups template; CAUSE: ${subError}`); diff --git a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts index 0d8c2a0c..f4e4e775 100644 --- a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts +++ b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts @@ -111,6 +111,6 @@ type AnvilDataUPSHash = { }; }; -type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { +type GetAnvilDataOptions = { predata?: Array<[string, ...unknown[]]>; }; From aa49de761a288984a7feaa041144d0a6f2e9544a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 18:00:48 -0400 Subject: [PATCH 67/81] fix(striker-ui-api): migrate sub to access interact --- striker-ui-api/src/lib/accessModule.ts | 3 +- .../request_handlers/command/runManifest.ts | 7 +-- .../host/createHostConnection.ts | 43 +++++++++++-------- .../manifest/buildManifest.ts | 37 +++++++++------- .../manifest/createManifest.ts | 4 +- .../manifest/deleteManifest.ts | 14 +++--- .../manifest/updateManifest.ts | 4 +- striker-ui-api/src/passport.ts | 20 +++++---- 8 files changed, 71 insertions(+), 61 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 6a92d3be..fa054b87 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -423,7 +423,6 @@ export { dbInsertOrUpdateVariable as variable, dbJobAnvilSyncShared, dbSubRefreshTimestamp as timestamp, - execModuleSubroutine as sub, getData, getFenceSpec, getHostData, @@ -433,6 +432,6 @@ export { getPeerData, getUpsSpec, query, - subroutine, + subroutine as sub, write, }; diff --git a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts index d3a6537c..c69dba54 100644 --- a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts @@ -122,7 +122,7 @@ export const runManifest: RequestHandler< const joinAnJobs: DBJobParams[] = []; - let anParams: Record | undefined; + let anParams: Record; try { anParams = Object.values(hostList).reduce>( @@ -163,8 +163,9 @@ export const runManifest: RequestHandler< } try { - const [newAnUuid] = sub('insert_or_update_anvils', { subParams: anParams }) - .stdout as [string]; + const [newAnUuid]: [string] = await sub('insert_or_update_anvils', { + params: [anParams], + }); joinAnJobs.forEach((jobParams) => { jobParams.job_data += `,anvil_uuid=${newAnUuid}`; diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index 4fe3b1a5..b31322cd 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -68,10 +68,10 @@ export const createHostConnection: RequestHandler< } try { - localIPAddress = sub('find_matching_ip', { - subModuleName: 'System', - subParams: { host: peerIPAddress }, - }).stdout; + [localIPAddress] = await sub('find_matching_ip', { + params: [{ host: peerIPAddress }], + pre: ['System'], + }); } catch (subError) { stderr(`Failed to get matching IP address; CAUSE: ${subError}`); @@ -89,15 +89,17 @@ export const createHostConnection: RequestHandler< stdoutVar({ pgpassFilePath, pgpassFileBody }); try { - sub('write_file', { - subModuleName: 'Storage', - subParams: { - body: pgpassFileBody, - file: pgpassFilePath, - mode: '0600', - overwrite: 1, - secure: 1, - }, + await sub('write_file', { + params: [ + { + body: pgpassFileBody, + file: pgpassFilePath, + mode: '0600', + overwrite: 1, + secure: 1, + }, + ], + pre: ['Storage'], }); } catch (subError) { stderr(`Failed to write ${pgpassFilePath}; CAUSE: ${subError}`); @@ -106,12 +108,15 @@ export const createHostConnection: RequestHandler< } try { - const [rawIsPeerDBReachable] = sub('call', { - subModuleName: 'System', - subParams: { - shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`, - }, - }).stdout as [output: string, returnCode: number]; + const [rawIsPeerDBReachable]: [output: string, returnCode: number] = + await sub('call', { + params: [ + { + shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`, + }, + ], + pre: ['System'], + }); isPeerDBReachable = rawIsPeerDBReachable === '1'; } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts index 249499d8..a6fcda08 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts @@ -13,7 +13,7 @@ import { sub } from '../../accessModule'; import { sanitize } from '../../sanitize'; import { stdout } from '../../shell'; -export const buildManifest = ( +export const buildManifest = async ( ...[request]: Parameters< RequestHandler< { manifestUuid?: string }, @@ -257,24 +257,29 @@ export const buildManifest = ( {}, ); - let result: { name: string; uuid: string } | undefined; + let result: { name: string; uuid: string }; try { - const [uuid, name] = sub('generate_manifest', { - subModuleName: 'Striker', - subParams: { - dns, - domain, - manifest_uuid: manifestUuid, - mtu, - ntp, - prefix, - sequence, - ...networkCountContainer, - ...networkContainer, - ...hostContainer, + const [uuid, name]: [manifestUuid: string, anvilName: string] = await sub( + 'generate_manifest', + { + params: [ + { + dns, + domain, + manifest_uuid: manifestUuid, + mtu, + ntp, + prefix, + sequence, + ...networkCountContainer, + ...networkContainer, + ...hostContainer, + }, + ], + pre: ['Striker'], }, - }).stdout as [manifestUuid: string, anvilName: string]; + ); result = { name, uuid }; } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts index b2c5331d..292e249f 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts @@ -4,13 +4,13 @@ import { RequestHandler } from 'express'; import { buildManifest } from './buildManifest'; import { stderr } from '../../shell'; -export const createManifest: RequestHandler = (...handlerArgs) => { +export const createManifest: RequestHandler = async (...handlerArgs) => { const [, response] = handlerArgs; let result: Record = {}; try { - result = buildManifest(...handlerArgs); + result = await buildManifest(...handlerArgs); } catch (buildError) { stderr(`Failed to create new install manifest; CAUSE ${buildError}`); diff --git a/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts index 0ba2d14b..870906e7 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts @@ -7,7 +7,7 @@ export const deleteManifest: RequestHandler< { manifestUuid: string }, undefined, { uuids: string[] } -> = (request, response) => { +> = async (request, response) => { const { params: { manifestUuid: rawManifestUuid }, body: { uuids: rawManifestUuidList } = {}, @@ -17,21 +17,19 @@ export const deleteManifest: RequestHandler< ? rawManifestUuidList : [rawManifestUuid]; - manifestUuidList.forEach((uuid) => { + for (const uuid of manifestUuidList) { stdout(`Begin delete manifest ${uuid}.`); try { - sub('insert_or_update_manifests', { - subParams: { delete: 1, manifest_uuid: uuid }, + await sub('insert_or_update_manifests', { + params: [{ delete: 1, manifest_uuid: uuid }], }); } catch (subError) { stderr(`Failed to delete manifest ${uuid}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); + } response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts b/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts index 728d0af7..9d583ac6 100644 --- a/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts @@ -4,7 +4,7 @@ import { RequestHandler } from 'express'; import { buildManifest } from './buildManifest'; import { stderr } from '../../shell'; -export const updateManifest: RequestHandler = (...args) => { +export const updateManifest: RequestHandler = async (...args) => { const [request, response] = args; const { params: { manifestUuid }, @@ -13,7 +13,7 @@ export const updateManifest: RequestHandler = (...args) => { let result: Record = {}; try { - result = buildManifest(...args); + result = await buildManifest(...args); } catch (buildError) { stderr( `Failed to update install manifest ${manifestUuid}; CAUSE: ${buildError}`, diff --git a/striker-ui-api/src/passport.ts b/striker-ui-api/src/passport.ts index 48d7f97e..66dba517 100644 --- a/striker-ui-api/src/passport.ts +++ b/striker-ui-api/src/passport.ts @@ -55,15 +55,17 @@ passport.use( }; try { - encryptResult = sub('encrypt_password', { - subModuleName: 'Account', - subParams: { - algorithm: userAlgorithm, - hash_count: userHashCount, - password, - salt: userSalt, - }, - }).stdout; + [encryptResult] = await sub('encrypt_password', { + params: [ + { + algorithm: userAlgorithm, + hash_count: userHashCount, + password, + salt: userSalt, + }, + ], + pre: ['Account'], + }); } catch (subError) { return done(subError); } From 3608d6e56e4f5dbbf9c8c636ae356c760e7f7f52 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 18:01:44 -0400 Subject: [PATCH 68/81] fix(striker-ui-api): accept manifest UUID as URI param in DELETE /manifest --- striker-ui-api/src/routes/manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui-api/src/routes/manifest.ts b/striker-ui-api/src/routes/manifest.ts index 6d554b43..88550032 100644 --- a/striker-ui-api/src/routes/manifest.ts +++ b/striker-ui-api/src/routes/manifest.ts @@ -13,7 +13,7 @@ const router = express.Router(); router .delete('/', deleteManifest) - .delete('/manifestUuid', deleteManifest) + .delete('/:manifestUuid', deleteManifest) .get('/', getManifest) .get('/template', getManifestTemplate) .get('/:manifestUUID', getManifestDetail) From 7d73fc931e34f679040203739fb7dda1df57f78b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 18:18:11 -0400 Subject: [PATCH 69/81] fix(striker-ui-api): migrate insert or update variable to access interact --- striker-ui-api/src/lib/accessModule.ts | 19 ++++++++++--------- striker-ui-api/src/lib/getSessionSecret.ts | 2 +- .../lib/request_handlers/host/prepareHost.ts | 16 ++++++---------- .../DBInsertOrUpdateVariableFunction.d.ts | 7 ++----- 4 files changed, 19 insertions(+), 25 deletions(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index fa054b87..0ef89bc2 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -270,14 +270,15 @@ const dbInsertOrUpdateJob = ( subParams: { job_progress, line, ...rest }, }).stdout; -const dbInsertOrUpdateVariable: DBInsertOrUpdateVariableFunction = ( - subParams, - { spawnSyncOptions } = {}, -) => - execModuleSubroutine('insert_or_update_variables', { - spawnSyncOptions, - subParams, - }).stdout; +const insertOrUpdateVariable: DBInsertOrUpdateVariableFunction = async ( + params, +) => { + const [uuid]: [string] = await subroutine('insert_or_update_variables', { + params: [params], + }); + + return uuid; +}; const dbJobAnvilSyncShared = ( jobName: string, @@ -420,7 +421,7 @@ const getUpsSpec = async () => { export { dbInsertOrUpdateJob as job, - dbInsertOrUpdateVariable as variable, + insertOrUpdateVariable as variable, dbJobAnvilSyncShared, dbSubRefreshTimestamp as timestamp, getData, diff --git a/striker-ui-api/src/lib/getSessionSecret.ts b/striker-ui-api/src/lib/getSessionSecret.ts index cc20ba04..46878e74 100644 --- a/striker-ui-api/src/lib/getSessionSecret.ts +++ b/striker-ui-api/src/lib/getSessionSecret.ts @@ -39,7 +39,7 @@ export const getSessionSecret = async (): Promise => { } try { - const vuuid = variable({ + const vuuid = await variable({ file: __filename, variable_name: VNAME_SESSION_SECRET, variable_value: sessionSecret, diff --git a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts index 3b402136..8031d438 100644 --- a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts +++ b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts @@ -6,8 +6,8 @@ import { REP_IPV4, REP_PEACEFUL_STRING, REP_UUID, -} from '../../consts/REG_EXP_PATTERNS'; -import SERVER_PATHS from '../../consts/SERVER_PATHS'; + SERVER_PATHS, +} from '../../consts'; import { job, variable } from '../../accessModule'; import { sanitize } from '../../sanitize'; @@ -17,7 +17,7 @@ export const prepareHost: RequestHandler< unknown, undefined, PrepareHostRequestBody -> = (request, response) => { +> = async (request, response) => { const { body: { enterpriseUUID, @@ -106,14 +106,12 @@ export const prepareHost: RequestHandler< `Failed to assert value when trying to prepare host; CAUSE: ${assertError}`, ); - response.status(400).send(); - - return; + return response.status(400).send(); } try { if (isHostUUIDProvided) { - variable({ + await variable({ file: __filename, update_value_only: 1, variable_name: 'system::configured', @@ -141,9 +139,7 @@ type=${dataHostType}`, } catch (subError) { stderr(`Failed to init host; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(200).send(); diff --git a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts b/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts index 07310ed9..bce32c5d 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts +++ b/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts @@ -10,9 +10,6 @@ type DBVariableParams = DBInsertOrUpdateFunctionCommonParams & { variable_value?: number | string; }; -type DBInsertOrUpdateVariableOptions = DBInsertOrUpdateFunctionCommonOptions; - type DBInsertOrUpdateVariableFunction = ( - subParams: DBVariableParams, - options?: DBInsertOrUpdateVariableOptions, -) => string; + params: DBVariableParams, +) => Promise; From d8cc7d9979a7fac46d958631567ea3d9f80e4868 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 19:00:41 -0400 Subject: [PATCH 70/81] fix(striker-ui-api): migrate job to access interact --- striker-ui-api/src/lib/accessModule.ts | 110 ++-------- .../command/buildHostPowerHandler.ts | 8 +- .../request_handlers/command/runManifest.ts | 23 +-- .../request_handlers/command/updateSystem.ts | 8 +- .../file/buildQueryFileDetail.ts | 8 +- .../lib/request_handlers/file/createFile.ts | 16 ++ .../lib/request_handlers/file/deleteFile.ts | 29 +++ .../src/lib/request_handlers/file/getFile.ts | 10 +- .../request_handlers/file/getFileDetail.ts | 6 +- .../src/lib/request_handlers/file/index.ts | 5 + .../lib/request_handlers/file/updateFile.ts | 134 ++++++++++++ .../request_handlers/host/configStriker.ts | 4 +- .../host/createHostConnection.ts | 2 +- .../host/deleteHostConnection.ts | 28 ++- .../lib/request_handlers/host/prepareHost.ts | 2 +- .../host/setHostInstallTarget.ts | 38 ++-- .../request_handlers/server/createServer.ts | 2 +- .../server/getServerDetail.ts | 2 +- .../ssh-key/deleteSSHKeyConflict.ts | 22 +- striker-ui-api/src/routes/file.ts | 191 +----------------- .../types/DBInsertOrUpdateFunctionCommon.d.ts | 5 - .../types/DBInsertOrUpdateJobFunction.d.ts | 2 - 22 files changed, 283 insertions(+), 372 deletions(-) create mode 100644 striker-ui-api/src/lib/request_handlers/file/createFile.ts create mode 100644 striker-ui-api/src/lib/request_handlers/file/deleteFile.ts create mode 100644 striker-ui-api/src/lib/request_handlers/file/index.ts create mode 100644 striker-ui-api/src/lib/request_handlers/file/updateFile.ts diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 0ef89bc2..03f644d6 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -1,10 +1,4 @@ -import { - ChildProcess, - spawn, - SpawnOptions, - spawnSync, - SpawnSyncOptions, -} from 'child_process'; +import { ChildProcess, spawn, SpawnOptions } from 'child_process'; import EventEmitter from 'events'; import { readFileSync } from 'fs'; @@ -186,90 +180,18 @@ const write = async (script: string) => { return wcode; }; -const execAnvilAccessModule = ( - args: string[], - options: SpawnSyncOptions = {}, -) => { - const { - encoding = 'utf-8', - timeout = 10000, - ...restSpawnSyncOptions - } = options; - - const { error, stderr, stdout } = spawnSync( - SERVER_PATHS.usr.sbin['anvil-access-module'].self, - args, - { encoding, timeout, ...restSpawnSyncOptions }, - ); - - if (error) { - throw error; - } - - if (stderr.length > 0) { - throw new Error(stderr.toString()); - } - - let output; - - try { - output = JSON.parse(stdout.toString()); - } catch (stdoutParseError) { - output = stdout; - - sherr( - `Failed to parse anvil-access-module stdout; CAUSE: ${stdoutParseError}`, - ); - } - - return { - stdout: output, - }; -}; - -const execModuleSubroutine = ( - subName: string, - { - spawnSyncOptions, - subModuleName, - subParams, - }: ExecModuleSubroutineOptions = {}, -) => { - const args = ['--sub', subName]; - - // Defaults to "Database" in anvil-access-module. - if (subModuleName) { - args.push('--sub-module', subModuleName); - } - - if (subParams) { - args.push('--sub-params', JSON.stringify(subParams)); - } - - shout( - `...${subModuleName}->${subName} with params: ${JSON.stringify( - subParams, - null, - 2, - )}`, - ); - - const { stdout } = execAnvilAccessModule(args, spawnSyncOptions); +const insertOrUpdateJob = async ({ + job_progress = 0, + line = 0, + ...rest +}: DBJobParams) => { + const [uuid]: [string] = await subroutine('insert_or_update_jobs', { + params: [{ job_progress, line, ...rest }], + }); - return { - stdout: stdout['sub_results'], - }; + return uuid; }; -const dbInsertOrUpdateJob = ( - { job_progress = 0, line = 0, ...rest }: DBJobParams, - { spawnSyncOptions }: DBInsertOrUpdateJobOptions = {}, -) => - execModuleSubroutine('insert_or_update_jobs', { - spawnSyncOptions, - subParams: { job_progress, line, ...rest }, - }).stdout; - const insertOrUpdateVariable: DBInsertOrUpdateVariableFunction = async ( params, ) => { @@ -280,7 +202,7 @@ const insertOrUpdateVariable: DBInsertOrUpdateVariableFunction = async ( return uuid; }; -const dbJobAnvilSyncShared = ( +const anvilSyncShared = ( jobName: string, jobData: string, jobTitle: string, @@ -300,10 +222,10 @@ const dbJobAnvilSyncShared = ( subParams.job_host_uuid = jobHostUUID; } - return dbInsertOrUpdateJob(subParams); + return insertOrUpdateJob(subParams); }; -const dbSubRefreshTimestamp = () => { +const refreshTimestamp = () => { let result: string; try { @@ -420,10 +342,10 @@ const getUpsSpec = async () => { }; export { - dbInsertOrUpdateJob as job, + insertOrUpdateJob as job, insertOrUpdateVariable as variable, - dbJobAnvilSyncShared, - dbSubRefreshTimestamp as timestamp, + anvilSyncShared, + refreshTimestamp as timestamp, getData, getFenceSpec, getHostData, diff --git a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts index b69c6377..e66b57cd 100644 --- a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts @@ -32,7 +32,7 @@ export const buildHostPowerHandler: ( task?: 'poweroff' | 'reboot', ) => RequestHandler = (task = 'reboot') => - (request, response) => { + async (request, response) => { const subParams: DBJobParams = { file: __filename, @@ -40,13 +40,11 @@ export const buildHostPowerHandler: ( }; try { - job(subParams); + await job(subParams); } catch (subError) { stderr(`Failed to ${task} host; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts index c69dba54..cc5ba341 100644 --- a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts @@ -81,9 +81,7 @@ export const runManifest: RequestHandler< assert(isHostListUnique, `Each entry in hosts must be unique`); } catch (assertError) { - handleAssertError(assertError); - - return; + return handleAssertError(assertError); } let rawHostListData: AnvilDataHostListHash | undefined; @@ -99,15 +97,11 @@ export const runManifest: RequestHandler< `Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, ); - response.status(500).send(); - - return; + return response.status(500).send(); } if (!rawHostListData || !rawManifestListData || !rawSysData) { - response.status(404).send(); - - return; + return response.status(404).send(); } const { host_uuid: hostUuidMapToData } = rawHostListData; @@ -167,16 +161,15 @@ export const runManifest: RequestHandler< params: [anParams], }); - joinAnJobs.forEach((jobParams) => { + for (const jobParams of joinAnJobs) { jobParams.job_data += `,anvil_uuid=${newAnUuid}`; - job(jobParams); - }); + + await job(jobParams); + } } catch (subError) { stderr(`Failed to record new anvil node entry; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts b/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts index d6ec70d7..d644f52a 100644 --- a/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts +++ b/striker-ui-api/src/lib/request_handlers/command/updateSystem.ts @@ -5,9 +5,9 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; import { stderr } from '../../shell'; -export const updateSystem: RequestHandler = (request, response) => { +export const updateSystem: RequestHandler = async (request, response) => { try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-update-system'].self, job_description: 'job_0004', @@ -17,9 +17,7 @@ export const updateSystem: RequestHandler = (request, response) => { } catch (subError) { stderr(`Failed to initiate system update; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } response.status(204).send(); diff --git a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts index 2c36e0bf..3fb2a89f 100644 --- a/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts @@ -1,7 +1,9 @@ +import { DELETED } from '../../consts'; + import join from '../../join'; import { stdoutVar } from '../../shell'; -const buildQueryFileDetail = ({ +export const buildQueryFileDetail = ({ fileUUIDs = ['*'], }: { fileUUIDs?: string[] | '*'; @@ -34,8 +36,6 @@ const buildQueryFileDetail = ({ ON fil.file_uuid = fil_loc.file_location_file_uuid JOIN anvils AS anv ON fil_loc.file_location_anvil_uuid = anv.anvil_uuid - WHERE fil.file_type != 'DELETED' + WHERE fil.file_type != '${DELETED}' ${condFileUUIDs};`; }; - -export default buildQueryFileDetail; diff --git a/striker-ui-api/src/lib/request_handlers/file/createFile.ts b/striker-ui-api/src/lib/request_handlers/file/createFile.ts new file mode 100644 index 00000000..991138e4 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/createFile.ts @@ -0,0 +1,16 @@ +import { RequestHandler } from 'express'; + +import { anvilSyncShared } from '../../accessModule'; +import { stdout, stdoutVar } from '../../shell'; + +export const createFile: RequestHandler = async ({ file, body }, response) => { + stdout('Receiving shared file.'); + + if (!file) return response.status(400).send(); + + stdoutVar({ body, file }); + + await anvilSyncShared('move_incoming', `file=${file.path}`, '0132', '0133'); + + response.status(201).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts b/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts new file mode 100644 index 00000000..c1c36705 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/deleteFile.ts @@ -0,0 +1,29 @@ +import { RequestHandler } from 'express'; + +import { DELETED } from '../../consts'; + +import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; + +export const deleteFile: RequestHandler = async (request, response) => { + const { fileUUID } = request.params; + + const [[oldFileType]] = await query( + `SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, + ); + + if (oldFileType !== DELETED) { + await write( + `UPDATE files + SET + file_type = '${DELETED}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`, + ); + + await anvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { + jobHostUUID: 'all', + }); + } + + response.status(204).send(); +}; diff --git a/striker-ui-api/src/lib/request_handlers/file/getFile.ts b/striker-ui-api/src/lib/request_handlers/file/getFile.ts index 3e28212c..f8876ea9 100644 --- a/striker-ui-api/src/lib/request_handlers/file/getFile.ts +++ b/striker-ui-api/src/lib/request_handlers/file/getFile.ts @@ -1,10 +1,12 @@ import { RequestHandler } from 'express'; +import { DELETED } from '../../consts'; + import buildGetRequestHandler from '../buildGetRequestHandler'; -import buildQueryFileDetail from './buildQueryFileDetail'; +import { buildQueryFileDetail } from './buildQueryFileDetail'; import { sanitize } from '../../sanitize'; -const getFile: RequestHandler = buildGetRequestHandler((request) => { +export const getFile: RequestHandler = buildGetRequestHandler((request) => { const { fileUUIDs } = request.query; let query = ` @@ -15,7 +17,7 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => { file_type, file_md5sum FROM files - WHERE file_type != 'DELETED';`; + WHERE file_type != '${DELETED}';`; if (fileUUIDs) { query = buildQueryFileDetail({ @@ -27,5 +29,3 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => { return query; }); - -export default getFile; diff --git a/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts b/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts index 36561213..18201dd2 100644 --- a/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts @@ -1,12 +1,10 @@ import { RequestHandler } from 'express'; import buildGetRequestHandler from '../buildGetRequestHandler'; -import buildQueryFileDetail from './buildQueryFileDetail'; +import { buildQueryFileDetail } from './buildQueryFileDetail'; import { sanitizeSQLParam } from '../../sanitizeSQLParam'; -const getFileDetail: RequestHandler = buildGetRequestHandler( +export const getFileDetail: RequestHandler = buildGetRequestHandler( ({ params: { fileUUID } }) => buildQueryFileDetail({ fileUUIDs: [sanitizeSQLParam(fileUUID)] }), ); - -export default getFileDetail; diff --git a/striker-ui-api/src/lib/request_handlers/file/index.ts b/striker-ui-api/src/lib/request_handlers/file/index.ts new file mode 100644 index 00000000..9eb9f73b --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/index.ts @@ -0,0 +1,5 @@ +export * from './createFile'; +export * from './deleteFile'; +export * from './getFile'; +export * from './getFileDetail'; +export * from './updateFile'; diff --git a/striker-ui-api/src/lib/request_handlers/file/updateFile.ts b/striker-ui-api/src/lib/request_handlers/file/updateFile.ts new file mode 100644 index 00000000..8e257b70 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/file/updateFile.ts @@ -0,0 +1,134 @@ +import { RequestHandler } from 'express'; + +import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; +import { stderr, stdoutVar } from '../../shell'; + +export const updateFile: RequestHandler = async (request, response) => { + const { body = {}, params } = request; + + stdoutVar(body, 'Begin edit single file. body='); + + const { fileUUID } = params; + const { fileName, fileLocations, fileType } = body; + const anvilSyncSharedFunctions = []; + + let sqlscript = ''; + + if (fileName) { + const [[oldFileName]] = await query( + `SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, + ); + + stdoutVar({ oldFileName, fileName }); + + if (fileName !== oldFileName) { + sqlscript += ` + UPDATE files + SET + file_name = '${fileName}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`; + + anvilSyncSharedFunctions.push(() => + anvilSyncShared( + 'rename', + `file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, + '0138', + '0139', + { jobHostUUID: 'all' }, + ), + ); + } + } + + if (fileType) { + sqlscript += ` + UPDATE files + SET + file_type = '${fileType}', + modified_date = '${timestamp()}' + WHERE file_uuid = '${fileUUID}';`; + + anvilSyncSharedFunctions.push(() => + anvilSyncShared('check_mode', `file_uuid=${fileUUID}`, '0143', '0144', { + jobHostUUID: 'all', + }), + ); + } + + if (fileLocations) { + fileLocations.forEach( + async ({ + fileLocationUUID, + isFileLocationActive, + }: { + fileLocationUUID: string; + isFileLocationActive: boolean; + }) => { + let fileLocationActive = 0; + let jobName = 'purge'; + let jobTitle = '0136'; + let jobDescription = '0137'; + + if (isFileLocationActive) { + fileLocationActive = 1; + jobName = 'pull_file'; + jobTitle = '0132'; + jobDescription = '0133'; + } + + sqlscript += ` + UPDATE file_locations + SET + file_location_active = '${fileLocationActive}', + modified_date = '${timestamp()}' + WHERE file_location_uuid = '${fileLocationUUID}';`; + + const targetHosts: [ + n1uuid: string, + n2uuid: string, + dr1uuid: null | string, + ][] = await query( + `SELECT + anv.anvil_node1_host_uuid, + anv.anvil_node2_host_uuid, + anv.anvil_dr1_host_uuid + FROM anvils AS anv + JOIN file_locations AS fil_loc + ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid + WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`, + ); + + targetHosts.flat().forEach((hostUUID: null | string) => { + if (hostUUID) { + anvilSyncSharedFunctions.push(() => + anvilSyncShared( + jobName, + `file_uuid=${fileUUID}`, + jobTitle, + jobDescription, + { jobHostUUID: hostUUID }, + ), + ); + } + }); + }, + ); + } + + let wcode: number; + + try { + wcode = await write(sqlscript); + } catch (queryError) { + stderr(`Failed to execute query; CAUSE: ${queryError}`); + + return response.status(500).send(); + } + + anvilSyncSharedFunctions.forEach(async (fn, index) => + stdoutVar(await fn(), `Anvil sync shared [${index}] output: `), + ); + + response.status(200).send(wcode); +}; diff --git a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts index fc68cac1..0b186177 100644 --- a/striker-ui-api/src/lib/request_handlers/host/configStriker.ts +++ b/striker-ui-api/src/lib/request_handlers/host/configStriker.ts @@ -41,7 +41,7 @@ export const configStriker: RequestHandler< unknown, undefined, Partial -> = (request, response) => { +> = async (request, response) => { const { body = {} } = request; stdoutVar(body, 'Begin initialize Striker; body='); @@ -116,7 +116,7 @@ export const configStriker: RequestHandler< } try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self, job_data: `${fvar(1, 'domain')}=${domainName} diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index b31322cd..c38cf815 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -159,7 +159,7 @@ export const createHostConnection: RequestHandler< const peerJobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${localHostUUID} --host ${localIPAddress} --port ${localDBPort} --ping ${commonPing}`; try { - job({ + await job({ file: __filename, job_command: jobCommand, job_data: `password=${commonPassword} diff --git a/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts index 3f23f7f4..2470bf05 100644 --- a/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts @@ -10,33 +10,31 @@ export const deleteHostConnection: RequestHandler< unknown, undefined, DeleteHostConnectionRequestBody -> = (request, response) => { +> = async (request, response) => { const { body } = request; - const hostUUIDs = Object.keys(body); + const hostUuids = Object.keys(body); - hostUUIDs.forEach((key) => { - const hostUUID = toHostUUID(key); - const peerHostUUIDs = body[key]; + for (const key of hostUuids) { + const hostUuid = toHostUUID(key); + const peerHostUuids = body[key]; - peerHostUUIDs.forEach((peerHostUUID) => { + for (const peerHostUuid of peerHostUuids) { try { - job({ + await job({ file: __filename, - job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUUID}`, + job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUuid}`, job_description: 'job_0014', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: 'striker-peer::delete', job_title: 'job_0013', }); } catch (subError) { - stderr(`Failed to delete peer ${peerHostUUID}; CAUSE: ${subError}`); + stderr(`Failed to delete peer ${peerHostUuid}; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); - }); + } + } response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts index 8031d438..326718af 100644 --- a/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts +++ b/striker-ui-api/src/lib/request_handlers/host/prepareHost.ts @@ -121,7 +121,7 @@ export const prepareHost: RequestHandler< }); } - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['striker-initialize-host'].self, job_data: `enterprise_uuid=${dataEnterpriseUUID} diff --git a/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts b/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts index d74c0b40..e6785dec 100644 --- a/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts +++ b/striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts @@ -4,36 +4,38 @@ import { LOCAL } from '../../consts/LOCAL'; import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; -import { stderr, stdout } from '../../shell'; - -export const setHostInstallTarget: RequestHandler = (request, response) => { - stdout( - `Begin set host install target.\n${JSON.stringify(request.body, null, 2)}`, - ); - - const { isEnableInstallTarget } = - request.body as SetHostInstallTargetRequestBody; - const { hostUUID: rawHostUUID } = request.params as UpdateHostParams; - const hostUUID: string | undefined = - rawHostUUID === LOCAL ? undefined : rawHostUUID; +import { stderr, stdoutVar } from '../../shell'; + +export const setHostInstallTarget: RequestHandler< + UpdateHostParams, + undefined, + SetHostInstallTargetRequestBody +> = async (request, response) => { + const { body, params } = request; + + stdoutVar(body, `Begin set host install target; body=`); + + const { isEnableInstallTarget } = body; + const { hostUUID: rHostUuid } = params; + + const hostUuid: string | undefined = + rHostUuid === LOCAL ? undefined : rHostUuid; const task = isEnableInstallTarget ? 'enable' : 'disable'; try { - job({ + await job({ file: __filename, job_command: `${SERVER_PATHS.usr.sbin['striker-manage-install-target'].self} --${task}`, job_description: 'job_0016', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: `install-target::${task}`, job_title: 'job_0015', }); } catch (subError) { stderr(`Failed to ${task} install target; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - response.status(200).send(); + response.status(204).send(); }; diff --git a/striker-ui-api/src/lib/request_handlers/server/createServer.ts b/striker-ui-api/src/lib/request_handlers/server/createServer.ts index 5c1cdc39..4be884f0 100644 --- a/striker-ui-api/src/lib/request_handlers/server/createServer.ts +++ b/striker-ui-api/src/lib/request_handlers/server/createServer.ts @@ -150,7 +150,7 @@ driver_iso=${driverIsoUuid}`; stdout(`provisionServerJobHostUUID=[${provisionServerJobHostUUID}]`); try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-provision-server'].self, job_data: provisionServerJobData, diff --git a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts index a9641700..befd04ef 100644 --- a/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts +++ b/striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts @@ -128,7 +128,7 @@ export const getServerDetail: RequestHandler = async (request, response) => { } try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self, job_data: `server-uuid=${serverUUID} diff --git a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts index f2e634c8..16aa9687 100644 --- a/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts +++ b/striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts @@ -10,32 +10,30 @@ export const deleteSSHKeyConflict: RequestHandler< unknown, undefined, DeleteSshKeyConflictRequestBody -> = (request, response) => { +> = async (request, response) => { const { body } = request; - const hostUUIDs = Object.keys(body); + const hostUuids = Object.keys(body); - hostUUIDs.forEach((key) => { - const hostUUID = toHostUUID(key); - const stateUUIDs = body[key]; + for (const uuid of hostUuids) { + const hostUuid = toHostUUID(uuid); + const stateUuids = body[uuid]; try { - job({ + await job({ file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-manage-keys'].self, - job_data: stateUUIDs.join(','), + job_data: stateUuids.join(','), job_description: 'job_0057', - job_host_uuid: hostUUID, + job_host_uuid: hostUuid, job_name: 'manage::broken_keys', job_title: 'job_0056', }); } catch (subError) { stderr(`Failed to delete bad SSH keys; CAUSE: ${subError}`); - response.status(500).send(); - - return; + return response.status(500).send(); } - }); + } response.status(204).send(); }; diff --git a/striker-ui-api/src/routes/file.ts b/striker-ui-api/src/routes/file.ts index cc64a7d3..29e875ed 100644 --- a/striker-ui-api/src/routes/file.ts +++ b/striker-ui-api/src/routes/file.ts @@ -1,194 +1,21 @@ import express from 'express'; -import { DELETED } from '../lib/consts'; - import { - dbJobAnvilSyncShared, - timestamp, - query, - write, -} from '../lib/accessModule'; -import getFile from '../lib/request_handlers/file/getFile'; -import getFileDetail from '../lib/request_handlers/file/getFileDetail'; + createFile, + deleteFile, + getFile, + getFileDetail, + updateFile, +} from '../lib/request_handlers/file'; import uploadSharedFiles from '../middlewares/uploadSharedFiles'; -import { stderr, stdout, stdoutVar } from '../lib/shell'; const router = express.Router(); router - .delete('/:fileUUID', async (request, response) => { - const { fileUUID } = request.params; - - const [[oldFileType]] = await query( - `SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`, - ); - - if (oldFileType !== DELETED) { - await write( - `UPDATE files - SET - file_type = '${DELETED}', - modified_date = '${timestamp()}' - WHERE file_uuid = '${fileUUID}';`, - ); - - dbJobAnvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', { - jobHostUUID: 'all', - }); - } - - response.status(204).send(); - }) + .delete('/:fileUUID', deleteFile) .get('/', getFile) .get('/:fileUUID', getFileDetail) - .post('/', uploadSharedFiles.single('file'), ({ file, body }, response) => { - stdout('Receiving shared file.'); - - if (file) { - stdoutVar({ body, file }); - - dbJobAnvilSyncShared( - 'move_incoming', - `file=${file.path}`, - '0132', - '0133', - ); - - response.status(200).send(); - } - }) - .put('/:fileUUID', async (request, response) => { - const { body = {}, params } = request; - - stdoutVar(body, 'Begin edit single file. body='); - - const { fileUUID } = params; - const { fileName, fileLocations, fileType } = body; - const anvilSyncSharedFunctions = []; - - let sqlscript = ''; - - if (fileName) { - const [[oldFileName]] = await query( - `SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`, - ); - - stdoutVar({ oldFileName, fileName }); - - if (fileName !== oldFileName) { - sqlscript += ` - UPDATE files - SET - file_name = '${fileName}', - modified_date = '${timestamp()}' - WHERE file_uuid = '${fileUUID}';`; - - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - 'rename', - `file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`, - '0138', - '0139', - { jobHostUUID: 'all' }, - ), - ); - } - } - - if (fileType) { - sqlscript += ` - UPDATE files - SET - file_type = '${fileType}', - modified_date = '${timestamp()}' - WHERE file_uuid = '${fileUUID}';`; - - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - 'check_mode', - `file_uuid=${fileUUID}`, - '0143', - '0144', - { jobHostUUID: 'all' }, - ), - ); - } - - if (fileLocations) { - fileLocations.forEach( - async ({ - fileLocationUUID, - isFileLocationActive, - }: { - fileLocationUUID: string; - isFileLocationActive: boolean; - }) => { - let fileLocationActive = 0; - let jobName = 'purge'; - let jobTitle = '0136'; - let jobDescription = '0137'; - - if (isFileLocationActive) { - fileLocationActive = 1; - jobName = 'pull_file'; - jobTitle = '0132'; - jobDescription = '0133'; - } - - sqlscript += ` - UPDATE file_locations - SET - file_location_active = '${fileLocationActive}', - modified_date = '${timestamp()}' - WHERE file_location_uuid = '${fileLocationUUID}';`; - - const targetHosts: [ - n1uuid: string, - n2uuid: string, - dr1uuid: null | string, - ][] = await query( - `SELECT - anv.anvil_node1_host_uuid, - anv.anvil_node2_host_uuid, - anv.anvil_dr1_host_uuid - FROM anvils AS anv - JOIN file_locations AS fil_loc - ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid - WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`, - ); - - targetHosts.flat().forEach((hostUUID: null | string) => { - if (hostUUID) { - anvilSyncSharedFunctions.push(() => - dbJobAnvilSyncShared( - jobName, - `file_uuid=${fileUUID}`, - jobTitle, - jobDescription, - { jobHostUUID: hostUUID }, - ), - ); - } - }); - }, - ); - } - - let wcode: number; - - try { - wcode = await write(sqlscript); - } catch (queryError) { - stderr(`Failed to execute query; CAUSE: ${queryError}`); - - return response.status(500).send(); - } - - anvilSyncSharedFunctions.forEach((fn, index) => - stdoutVar(fn(), `Anvil sync shared [${index}] output: `), - ); - - response.status(200).send(wcode); - }); + .post('/', uploadSharedFiles.single('file'), createFile) + .put('/:fileUUID', updateFile); export default router; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts b/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts index 12be0674..20e39370 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts +++ b/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts @@ -2,8 +2,3 @@ type DBInsertOrUpdateFunctionCommonParams = ModuleSubroutineCommonParams & { file: string; line?: number; }; - -type DBInsertOrUpdateFunctionCommonOptions = Omit< - ExecModuleSubroutineOptions, - 'subParams' | 'subModuleName' ->; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts b/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts index 8fff8766..60f05a23 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts +++ b/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts @@ -7,5 +7,3 @@ type DBJobParams = DBInsertOrUpdateFunctionCommonParams & { job_host_uuid?: string; job_progress?: number; }; - -type DBInsertOrUpdateJobOptions = DBInsertOrUpdateFunctionCommonOptions; From 82f4bdff041f45146fc632aa828622182dedc192 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 20:37:33 -0400 Subject: [PATCH 71/81] refactor(striker-ui-api): organize types after complete migrate to access interact --- striker-ui-api/src/lib/accessModule.ts | 12 +++---- .../command/buildHostPowerHandler.ts | 10 +++--- .../request_handlers/command/runManifest.ts | 2 +- striker-ui-api/src/types/AccessModule.d.ts | 34 +++++-------------- .../src/types/AnvilSyncSharedFunction.d.ts | 3 ++ ...ts => BuildGetRequestHandlerFunction.d.ts} | 0 .../{CallOptions.d.ts => CallFunction.d.ts} | 0 .../types/DBInsertOrUpdateFunctionCommon.d.ts | 4 --- .../types/DBJobAnvilSyncSharedOptions.d.ts | 3 -- .../types/ExecModuleSubroutineFunction.d.ts | 5 --- .../src/types/GetPeerDataFunction.d.ts | 2 +- ...on.d.ts => InsertOrUpdateJobFunction.d.ts} | 2 +- ...ts => InsertOrUpdateVariableFunction.d.ts} | 6 ++-- .../types/ModuleSubroutineCommonParams.d.ts | 3 -- 14 files changed, 26 insertions(+), 60 deletions(-) create mode 100644 striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts rename striker-ui-api/src/types/{BuildGetRequestHandlerOptions.d.ts => BuildGetRequestHandlerFunction.d.ts} (100%) rename striker-ui-api/src/types/{CallOptions.d.ts => CallFunction.d.ts} (100%) delete mode 100644 striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts delete mode 100644 striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts delete mode 100644 striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts rename striker-ui-api/src/types/{DBInsertOrUpdateJobFunction.d.ts => InsertOrUpdateJobFunction.d.ts} (73%) rename striker-ui-api/src/types/{DBInsertOrUpdateVariableFunction.d.ts => InsertOrUpdateVariableFunction.d.ts} (69%) delete mode 100644 striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 03f644d6..6ddecddd 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -13,10 +13,6 @@ import { uuid, } from './shell'; -type AccessStartOptions = { - args?: readonly string[]; -} & SpawnOptions; - class Access extends EventEmitter { private ps: ChildProcess; private queue: string[] = []; @@ -184,7 +180,7 @@ const insertOrUpdateJob = async ({ job_progress = 0, line = 0, ...rest -}: DBJobParams) => { +}: JobParams) => { const [uuid]: [string] = await subroutine('insert_or_update_jobs', { params: [{ job_progress, line, ...rest }], }); @@ -192,7 +188,7 @@ const insertOrUpdateJob = async ({ return uuid; }; -const insertOrUpdateVariable: DBInsertOrUpdateVariableFunction = async ( +const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async ( params, ) => { const [uuid]: [string] = await subroutine('insert_or_update_variables', { @@ -207,9 +203,9 @@ const anvilSyncShared = ( jobData: string, jobTitle: string, jobDescription: string, - { jobHostUUID }: DBJobAnvilSyncSharedOptions = { jobHostUUID: undefined }, + { jobHostUUID }: JobAnvilSyncSharedOptions = { jobHostUUID: undefined }, ) => { - const subParams: DBJobParams = { + const subParams: JobParams = { file: __filename, job_command: SERVER_PATHS.usr.sbin['anvil-sync-shared'].self, job_data: jobData, diff --git a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts index e66b57cd..842f0d51 100644 --- a/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts +++ b/striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts @@ -5,14 +5,14 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS'; import { job } from '../../accessModule'; import { stderr } from '../../shell'; -type DistinctDBJobParams = Omit< - DBJobParams, +type DistinctJobParams = Omit< + JobParams, 'file' | 'line' | 'job_data' | 'job_progress' >; const MANAGE_HOST_POWER_JOB_PARAMS: { - poweroff: DistinctDBJobParams; - reboot: DistinctDBJobParams; + poweroff: DistinctJobParams; + reboot: DistinctJobParams; } = { poweroff: { job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`, @@ -33,7 +33,7 @@ export const buildHostPowerHandler: ( ) => RequestHandler = (task = 'reboot') => async (request, response) => { - const subParams: DBJobParams = { + const subParams: JobParams = { file: __filename, ...MANAGE_HOST_POWER_JOB_PARAMS[task], diff --git a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts index cc5ba341..e6f52447 100644 --- a/striker-ui-api/src/lib/request_handlers/command/runManifest.ts +++ b/striker-ui-api/src/lib/request_handlers/command/runManifest.ts @@ -114,7 +114,7 @@ export const runManifest: RequestHandler< } = rawManifestListData; const { hosts: { by_uuid: mapToHostNameData = {} } = {} } = rawSysData; - const joinAnJobs: DBJobParams[] = []; + const joinAnJobs: JobParams[] = []; let anParams: Record; diff --git a/striker-ui-api/src/types/AccessModule.d.ts b/striker-ui-api/src/types/AccessModule.d.ts index c95732e6..2825647d 100644 --- a/striker-ui-api/src/types/AccessModule.d.ts +++ b/striker-ui-api/src/types/AccessModule.d.ts @@ -1,30 +1,12 @@ -type AsyncAnvilAccessModuleCloseArgs = { - ecode: number | null; - signal: NodeJS.Signals | null; - stderr: string; - stdout: unknown; -}; - -type AsyncDatabaseWriteCloseArgs = AsyncAnvilAccessModuleCloseArgs & { - wcode: number | null; -}; - -type AsyncAnvilAccessModuleCloseHandler = ( - args: AsyncAnvilAccessModuleCloseArgs, -) => void; - -type AsyncDatabaseWriteCloseHandler = ( - args: AsyncDatabaseWriteCloseArgs, -) => void; +type AccessStartOptions = { + args?: readonly string[]; +} & import('child_process').SpawnOptions; -type AsyncAnvilAccessModuleOptions = import('child_process').SpawnOptions & { - onClose?: AsyncAnvilAccessModuleCloseHandler; - onError?: (err: Error) => void; +type SubroutineCommonParams = { + debug?: number; }; -type AsyncDatabaseWriteOptions = Omit< - AsyncAnvilAccessModuleOptions, - 'onClose' -> & { - onClose?: AsyncDatabaseWriteCloseHandler; +type InsertOrUpdateFunctionCommonParams = SubroutineCommonParams & { + file: string; + line?: number; }; diff --git a/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts b/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts new file mode 100644 index 00000000..12bd12d5 --- /dev/null +++ b/striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts @@ -0,0 +1,3 @@ +type JobAnvilSyncSharedOptions = { + jobHostUUID?: string; +}; diff --git a/striker-ui-api/src/types/BuildGetRequestHandlerOptions.d.ts b/striker-ui-api/src/types/BuildGetRequestHandlerFunction.d.ts similarity index 100% rename from striker-ui-api/src/types/BuildGetRequestHandlerOptions.d.ts rename to striker-ui-api/src/types/BuildGetRequestHandlerFunction.d.ts diff --git a/striker-ui-api/src/types/CallOptions.d.ts b/striker-ui-api/src/types/CallFunction.d.ts similarity index 100% rename from striker-ui-api/src/types/CallOptions.d.ts rename to striker-ui-api/src/types/CallFunction.d.ts diff --git a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts b/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts deleted file mode 100644 index 20e39370..00000000 --- a/striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -type DBInsertOrUpdateFunctionCommonParams = ModuleSubroutineCommonParams & { - file: string; - line?: number; -}; diff --git a/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts b/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts deleted file mode 100644 index e0251591..00000000 --- a/striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type DBJobAnvilSyncSharedOptions = { - jobHostUUID?: string; -}; diff --git a/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts b/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts deleted file mode 100644 index c151524e..00000000 --- a/striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -type ExecModuleSubroutineOptions = { - spawnSyncOptions?: import('child_process').SpawnSyncOptions; - subModuleName?: string; - subParams?: Record; -}; diff --git a/striker-ui-api/src/types/GetPeerDataFunction.d.ts b/striker-ui-api/src/types/GetPeerDataFunction.d.ts index 62a32791..76c16d6e 100644 --- a/striker-ui-api/src/types/GetPeerDataFunction.d.ts +++ b/striker-ui-api/src/types/GetPeerDataFunction.d.ts @@ -6,7 +6,7 @@ type PeerDataHash = { os_registered: string; }; -type GetPeerDataOptions = ModuleSubroutineCommonParams & { +type GetPeerDataOptions = SubroutineCommonParams & { password?: string; port?: number; }; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts b/striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts similarity index 73% rename from striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts rename to striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts index 60f05a23..580a7a5a 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateJobFunction.d.ts +++ b/striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts @@ -1,4 +1,4 @@ -type DBJobParams = DBInsertOrUpdateFunctionCommonParams & { +type JobParams = InsertOrUpdateFunctionCommonParams & { job_command: string; job_data?: string; job_name: string; diff --git a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts b/striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts similarity index 69% rename from striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts rename to striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts index bce32c5d..83e5c59c 100644 --- a/striker-ui-api/src/types/DBInsertOrUpdateVariableFunction.d.ts +++ b/striker-ui-api/src/types/InsertOrUpdateVariableFunction.d.ts @@ -1,4 +1,4 @@ -type DBVariableParams = DBInsertOrUpdateFunctionCommonParams & { +type VariableParams = InsertOrUpdateFunctionCommonParams & { update_value_only?: 0 | 1; variable_default?: string; varaible_description?: string; @@ -10,6 +10,6 @@ type DBVariableParams = DBInsertOrUpdateFunctionCommonParams & { variable_value?: number | string; }; -type DBInsertOrUpdateVariableFunction = ( - params: DBVariableParams, +type InsertOrUpdateVariableFunction = ( + params: VariableParams, ) => Promise; diff --git a/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts b/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts deleted file mode 100644 index 368b28f5..00000000 --- a/striker-ui-api/src/types/ModuleSubroutineCommonParams.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ModuleSubroutineCommonParams = { - debug?: number; -}; From 9a0ca2f15997db112102dddbf330cc98eed0acba Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 27 Apr 2023 20:46:02 -0400 Subject: [PATCH 72/81] docs(striker-ui-api): label anvil-access-module daemon start sequence --- striker-ui-api/src/lib/accessModule.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 6ddecddd..c006f5eb 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -37,7 +37,10 @@ class Access extends EventEmitter { uid = PUID, ...restSpawnOptions }: AccessStartOptions = {}) { - shvar({ gid, stdio, timeout, uid, ...restSpawnOptions }); + shvar( + { gid, stdio, timeout, uid, ...restSpawnOptions }, + `Starting anvil-access-module daemon with options: `, + ); const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, { gid, From 2eb5b8a094ce8557a639c57ea1b648c7035519d0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 28 Apr 2023 18:45:27 -0400 Subject: [PATCH 73/81] fix(striker-ui-api): make login respond with no-content (204) --- striker-ui-api/src/lib/request_handlers/auth/login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui-api/src/lib/request_handlers/auth/login.ts b/striker-ui-api/src/lib/request_handlers/auth/login.ts index ee4c99fd..4dbd272b 100644 --- a/striker-ui-api/src/lib/request_handlers/auth/login.ts +++ b/striker-ui-api/src/lib/request_handlers/auth/login.ts @@ -14,5 +14,5 @@ export const login: RequestHandler = ( stdout(`Successfully authenticated user [${userName}]`); } - response.status(200).send(); + response.status(204).send(); }; From dc765b3719e35b9711e81c9666a2fe6c1fc2b9e0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 28 Apr 2023 19:54:31 -0400 Subject: [PATCH 74/81] fix(striker-ui-api): set session cookie options * Set 'secure' because we don't have a certificate yet * Set 'httpOnly' to avoid exposing the cookie in 'document' --- striker-ui-api/src/session.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index 1d9b1133..d127889c 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -182,7 +182,11 @@ export class SessionStore extends BaseSessionStore { export default (async () => expressSession({ - cookie: { maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE }, + cookie: { + httpOnly: true, + maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, + secure: false, + }, genid: ({ path }) => { const sid = uuid(); From 4bbdcc1687263fdc6645ef5dd5adbc14b0a008cb Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 28 Apr 2023 20:09:19 -0400 Subject: [PATCH 75/81] fix(striker-ui-api): enable credentials in cors middleware --- striker-ui-api/src/app.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index c50193ca..39773fab 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -6,13 +6,23 @@ import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; import session from './session'; +import { stdout } from './lib/shell'; export default (async () => { const app = express(); app.use(json()); - app.use(cors()); + app.use( + cors({ + origin: (requestOrigin, done) => { + stdout(`Request header: Origin=${requestOrigin}`); + + done(null, requestOrigin); + }, + credentials: true, + }), + ); // Add session handler to the chain **after** adding other handlers that do // not depend on session(s). From 0f223950e4b4ea6a77a18bf0cd8cfdb18cc96ce3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 28 Apr 2023 20:17:02 -0400 Subject: [PATCH 76/81] fix(striker-ui): correct /login to post with-credentials enabled --- striker-ui/components/GatePanel.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/striker-ui/components/GatePanel.tsx b/striker-ui/components/GatePanel.tsx index ec483387..e3ddd016 100644 --- a/striker-ui/components/GatePanel.tsx +++ b/striker-ui/components/GatePanel.tsx @@ -26,7 +26,11 @@ const GatePanel: FC = () => { setIsSubmitting(true); api - .post('/auth/login', { username, password }) + .post( + '/auth/login', + { username, password }, + { withCredentials: true }, + ) .then(() => { router.push('/'); }) @@ -34,6 +38,7 @@ const GatePanel: FC = () => { const emsg = handleAPIError(error, { onResponseErrorAppend: () => ({ children: `Credentials mismatched.`, + type: 'warning', }), }); From feae6303708fbb0fff4f80fd4a2f5c0a48f68f11 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 2 May 2023 02:10:47 -0400 Subject: [PATCH 77/81] fix(striker-ui-api): allow override fail/succeed in assert authentication --- .../src/lib/assertAuthentication.ts | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/striker-ui-api/src/lib/assertAuthentication.ts b/striker-ui-api/src/lib/assertAuthentication.ts index 4abfb58f..b9d17afc 100644 --- a/striker-ui-api/src/lib/assertAuthentication.ts +++ b/striker-ui-api/src/lib/assertAuthentication.ts @@ -1,38 +1,54 @@ -import { Handler, Request, Response } from 'express'; +import { Handler } from 'express'; import { stdout } from './shell'; -export const assertAuthentication: (options?: { - failureRedirect?: string; - failureReturnTo?: boolean | string; -}) => Handler = ({ failureRedirect, failureReturnTo } = {}) => { - const redirectOnFailure: (response: Response) => void = failureRedirect - ? (response) => response.redirect(failureRedirect) - : (response) => response.status(404).send(); - - let getSessionReturnTo: ((request: Request) => string) | undefined; +type AssertAuthenticationOptions = { + fail?: string | ((...args: Parameters) => void); + failReturnTo?: boolean | string; + succeed?: string | ((...args: Parameters) => void); +}; - if (failureReturnTo === true) { - getSessionReturnTo = ({ originalUrl, url }) => originalUrl || url; - } else if (typeof failureReturnTo === 'string') { - getSessionReturnTo = () => failureReturnTo; +type AssertAuthenticationFunction = ( + options?: AssertAuthenticationOptions, +) => Handler; + +export const assertAuthentication: AssertAuthenticationFunction = ({ + fail: initFail = (request, response) => response.status(404).send(), + failReturnTo, + succeed: initSucceed = (request, response, next) => next(), +}: AssertAuthenticationOptions = {}) => { + const fail: (...args: Parameters) => void = + typeof initFail === 'string' + ? (request, response) => response.redirect(initFail) + : initFail; + + const succeed: (...args: Parameters) => void = + typeof initSucceed === 'string' + ? (request, response) => response.redirect(initSucceed) + : initSucceed; + + let getReturnTo: ((...args: Parameters) => string) | undefined; + + if (failReturnTo === true) { + getReturnTo = ({ originalUrl, url }) => originalUrl || url; + } else if (typeof failReturnTo === 'string') { + getReturnTo = () => failReturnTo; } - return (request, response, next) => { + return (...args) => { + const { 0: request } = args; const { originalUrl, session } = request; const { passport } = session; - if (!passport?.user) { - session.returnTo = getSessionReturnTo?.call(null, request); + if (passport?.user) return succeed(...args); - stdout( - `Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, - ); + session.returnTo = getReturnTo?.call(null, ...args); - return redirectOnFailure?.call(null, response); - } + stdout( + `Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, + ); - next(); + return fail(...args); }; }; From 5d54b5d855f8af96a2ebb4ef239dcf8138d9ff5f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 2 May 2023 02:21:16 -0400 Subject: [PATCH 78/81] fix(striker-ui-api): protect static files --- striker-ui-api/src/routes/static.ts | 39 ++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/routes/static.ts b/striker-ui-api/src/routes/static.ts index c06579e2..12585606 100644 --- a/striker-ui-api/src/routes/static.ts +++ b/striker-ui-api/src/routes/static.ts @@ -1,11 +1,48 @@ import express from 'express'; +import { existsSync } from 'fs'; +import path from 'path'; import { SERVER_PATHS } from '../lib/consts'; +import { assertAuthentication } from '../lib/assertAuthentication'; +import { stdout } from '../lib/shell'; + const router = express.Router(); +const htmlDir = SERVER_PATHS.var.www.html.self; + router.use( - express.static(SERVER_PATHS.var.www.html.self, { + (...args) => { + const { 0: request, 2: next } = args; + const { originalUrl } = request; + + if (/^[/]login/.test(originalUrl)) { + stdout(`Static:login requested`); + + return assertAuthentication({ fail: (rq, rs, nx) => nx(), succeed: '/' })( + ...args, + ); + } + + const parts = originalUrl.replace(/[/]$/, '').split('/'); + const tail = parts.pop() || 'index'; + + parts.push(`${tail}.html`); + + const htmlPath = path.posix.join(htmlDir, ...parts); + const isHtmlExists = existsSync(htmlPath); + + if (isHtmlExists) { + stdout(`Static:[${htmlPath}] requested`); + + return assertAuthentication({ fail: '/login', failReturnTo: true })( + ...args, + ); + } + + return next(); + }, + express.static(htmlDir, { extensions: ['htm', 'html'], }), ); From 32ae8dd41627ff7bb261c7e0e3e992c413c06e47 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 2 May 2023 02:35:12 -0400 Subject: [PATCH 79/81] fix(striker-ui-api): handle set session without user --- striker-ui-api/src/session.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/session.ts b/striker-ui-api/src/session.ts index d127889c..77fd635e 100644 --- a/striker-ui-api/src/session.ts +++ b/striker-ui-api/src/session.ts @@ -99,11 +99,11 @@ export class SessionStore extends BaseSessionStore { ): Promise { stdoutVar({ session }, `Set session ${sid}`); - const { - passport: { user: userUuid }, - } = session; + const { passport: { user: userUuid } = {} } = session; try { + assert.ok(userUuid, 'Missing user identifier'); + const localHostUuid = getLocalHostUUID(); const modifiedDate = timestamp(); From bfadd3bacbffe73e328c5b5ca048b04036e02fa4 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 2 May 2023 13:05:02 -0400 Subject: [PATCH 80/81] fix(striker-ui-api): use default cors origin handler --- striker-ui-api/src/app.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/striker-ui-api/src/app.ts b/striker-ui-api/src/app.ts index 39773fab..5ee5d262 100644 --- a/striker-ui-api/src/app.ts +++ b/striker-ui-api/src/app.ts @@ -6,7 +6,6 @@ import passport from './passport'; import routes from './routes'; import { rrouters } from './lib/rrouters'; import session from './session'; -import { stdout } from './lib/shell'; export default (async () => { const app = express(); @@ -15,11 +14,7 @@ export default (async () => { app.use( cors({ - origin: (requestOrigin, done) => { - stdout(`Request header: Origin=${requestOrigin}`); - - done(null, requestOrigin); - }, + origin: true, credentials: true, }), ); From c6bcbc952be67486f08c2b0bbc88cc2ced2f21f3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 2 May 2023 13:05:55 -0400 Subject: [PATCH 81/81] fix(striker-ui-api): handle static file requests with file extension --- striker-ui-api/src/routes/static.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/striker-ui-api/src/routes/static.ts b/striker-ui-api/src/routes/static.ts index 12585606..414f8195 100644 --- a/striker-ui-api/src/routes/static.ts +++ b/striker-ui-api/src/routes/static.ts @@ -26,8 +26,9 @@ router.use( const parts = originalUrl.replace(/[/]$/, '').split('/'); const tail = parts.pop() || 'index'; + const extended = /[.]html$/.test(tail) ? tail : `${tail}.html`; - parts.push(`${tail}.html`); + parts.push(extended); const htmlPath = path.posix.join(htmlDir, ...parts); const isHtmlExists = existsSync(htmlPath);