From f83c0d4260b7929670c61cd5146846ee29353bf8 Mon Sep 17 00:00:00 2001 From: dat Date: Tue, 12 Aug 2025 14:23:21 +0700 Subject: [PATCH] [72HR] Topcoder Admin App - Add Non-MM Submission Management --- package.json | 1 + .../ManageSubmissionPage.tsx | 39 ++- src/apps/admin/src/config/busEvent.config.ts | 24 +- .../ChallengeList/ChallengeList.tsx | 20 +- .../SubmissionTable/ShowHistoryButton.tsx | 31 ++ .../SubmissionTable.module.scss | 4 + .../SubmissionTable/SubmissionTable.tsx | 280 +++++++++++------- .../SubmissionTableActionsNonMM.module.scss | 16 + .../SubmissionTableActionsNonMM.tsx | 45 +++ src/apps/admin/src/lib/hooks/index.ts | 3 + .../src/lib/hooks/useDownloadSubmission.ts | 62 ++++ .../admin/src/lib/hooks/useFetchChallenge.ts | 51 ++++ .../admin/src/lib/hooks/useManageAVScan.ts | 71 +++++ .../lib/models/CommonRequestBusAPI.type.ts | 7 + .../lib/models/RequestBusAPIAVScan.model.ts | 21 ++ .../lib/models/ValidateS3URIResult.model.ts | 8 + src/apps/admin/src/lib/models/index.ts | 3 + .../src/lib/services/bus-event.service.ts | 6 +- .../src/lib/services/submissions.service.ts | 46 ++- src/apps/admin/src/lib/utils/challenge.ts | 2 +- src/apps/admin/src/lib/utils/others.ts | 32 ++ src/config/environments/default.env.ts | 6 + .../environments/global-config.model.ts | 6 + src/config/environments/prod.env.ts | 6 + yarn.lock | 5 + 25 files changed, 662 insertions(+), 133 deletions(-) create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/ShowHistoryButton.tsx create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.module.scss create mode 100644 src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.tsx create mode 100644 src/apps/admin/src/lib/hooks/useDownloadSubmission.ts create mode 100644 src/apps/admin/src/lib/hooks/useFetchChallenge.ts create mode 100644 src/apps/admin/src/lib/hooks/useManageAVScan.ts create mode 100644 src/apps/admin/src/lib/models/CommonRequestBusAPI.type.ts create mode 100644 src/apps/admin/src/lib/models/RequestBusAPIAVScan.model.ts create mode 100644 src/apps/admin/src/lib/models/ValidateS3URIResult.model.ts diff --git a/package.json b/package.json index 0046f4f9b..d660a2a5e 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@stripe/stripe-js": "1.41.0", "@tinymce/tinymce-react": "^6.2.1", "@types/codemirror": "5.60.15", + "amazon-s3-uri": "^0.1.1", "apexcharts": "^3.36.0", "axios": "^1.7.9", "browser-cookies": "^1.2.0", diff --git a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx index ab9a2abf1..bbc6a4c25 100644 --- a/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx +++ b/src/apps/admin/src/challenge-management/ManageSubmissionPage/ManageSubmissionPage.tsx @@ -1,13 +1,19 @@ /** * Manage Submission Page. */ -import { FC } from 'react' +import { FC, useMemo } from 'react' import { useParams } from 'react-router-dom' import classNames from 'classnames' import { LinkButton } from '~/libs/ui' import { + useDownloadSubmission, + useDownloadSubmissionProps, + useFetchChallenge, + useFetchChallengeProps, + useManageAVScan, + useManageAVScanProps, useManageBusEvent, useManageBusEventProps, useManageChallengeSubmissions, @@ -20,6 +26,7 @@ import { TableLoading, TableNoRecord, } from '../../lib' +import { checkIsMM } from '../../lib/utils' import styles from './ManageSubmissionPage.module.scss' @@ -35,7 +42,13 @@ export const ManageSubmissionPage: FC = (props: Props) => { = useManageBusEvent() const { - isLoading, + isLoading: isLoadingChallenge, + challengeInfo, + }: useFetchChallengeProps = useFetchChallenge(challengeId) + const isMM = useMemo(() => checkIsMM(challengeInfo), [challengeInfo]) + + const { + isLoading: isLoadingSubmission, submissions, isRemovingSubmission, isRemovingSubmissionBool, @@ -48,6 +61,19 @@ export const ManageSubmissionPage: FC = (props: Props) => { }: useManageChallengeSubmissionsProps = useManageChallengeSubmissions(challengeId) + const { + isLoading: isDownloadingSubmission, + isLoadingBool: isDownloadingSubmissionBool, + downloadSubmission, + }: useDownloadSubmissionProps = useDownloadSubmission() + const { + isLoading: isDoingAvScan, + isLoadingBool: isDoingAvScanBool, + doPostBusEvent: doPostBusEventAvScan, + }: useManageAVScanProps = useManageAVScan() + + const isLoading = isLoadingSubmission || isLoadingChallenge + return ( = (props: Props) => { ) : (
= (props: Props) => { doPostBusEvent={doPostBusEvent} showSubmissionHistory={showSubmissionHistory} setShowSubmissionHistory={setShowSubmissionHistory} + isMM={isMM} /> - {(isRemovingSubmissionBool + {(isDoingAvScanBool + || isDownloadingSubmissionBool + || isRemovingSubmissionBool || isRunningTestBool || isRemovingReviewSummationsBool) && ( diff --git a/src/apps/admin/src/config/busEvent.config.ts b/src/apps/admin/src/config/busEvent.config.ts index 464b198a0..85888b5dc 100644 --- a/src/apps/admin/src/config/busEvent.config.ts +++ b/src/apps/admin/src/config/busEvent.config.ts @@ -3,10 +3,14 @@ */ import { v4 as uuidv4 } from 'uuid' -import { RequestBusAPI } from '../lib/models' +import { + RequestBusAPI, + RequestBusAPIAVScan, + RequestBusAPIAVScanPayload, +} from '../lib/models' /** - * Create data for bus event + * Create data for data submission marathon match bus event * @param submissionId submission id * @param testType test type * @returns data for bus event @@ -27,3 +31,19 @@ export const CREATE_BUS_EVENT_DATA_SUBMISSION_MARATHON_MATCH = ( .toISOString(), topic: 'submission.notification.score', }) + +/** + * Create data for av rescan bus event + * @param payload av rescan payload + * @returns data for bus event + */ +export const CREATE_BUS_EVENT_AV_RESCAN = ( + payload: RequestBusAPIAVScanPayload, +): RequestBusAPIAVScan => ({ + 'mime-type': 'application/json', + originator: 'submission-processor', + payload, + timestamp: new Date() + .toISOString(), + topic: 'avscan.action.scan', +}) diff --git a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx index 7e20829f9..de3fcb6af 100644 --- a/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx +++ b/src/apps/admin/src/lib/components/ChallengeList/ChallengeList.tsx @@ -21,7 +21,6 @@ import { import { useEventCallback } from '../../hooks' import { Challenge, ChallengeFilterCriteria, ChallengeType } from '../../models' import { Paging } from '../../models/challenge-management/Pagination' -import { checkIsMM } from '../../utils' import { MobileListView } from './MobileListView' import styles from './ChallengeList.module.scss' @@ -135,7 +134,6 @@ const Actions: FC<{ challenge: Challenge currentFilters: ChallengeFilterCriteria }> = props => { - const isMM = useMemo(() => checkIsMM(props.challenge), [props.challenge]) const [openDropdown, setOpenDropdown] = useState(false) const navigate = useNavigate() const goToManageUser = useEventCallback(() => { @@ -202,16 +200,14 @@ const Actions: FC<{ > Users - {isMM && ( -
  • - Submissions -
  • - )} +
  • + Submissions +
  • diff --git a/src/apps/admin/src/lib/components/SubmissionTable/ShowHistoryButton.tsx b/src/apps/admin/src/lib/components/SubmissionTable/ShowHistoryButton.tsx new file mode 100644 index 000000000..43e17b342 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/ShowHistoryButton.tsx @@ -0,0 +1,31 @@ +/** + * Submission Table Actions For Non MM Challenge. + */ +import { Dispatch, FC, SetStateAction } from 'react' + +import { Button } from '~/libs/ui' + +import { IsRemovingType, Submission } from '../../models' + +interface Props { + data: Submission + showSubmissionHistory: IsRemovingType + setShowSubmissionHistory: Dispatch> +} + +export const ShowHistoryButton: FC = (props: Props) => ( + +) + +export default ShowHistoryButton diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss index e6e7556f2..4b3979e46 100644 --- a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss @@ -13,6 +13,10 @@ .rowActions { display: flex; align-items: center; + + @include ltelg { + flex-wrap: wrap; + } } .desktopTable { diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx index bdf462125..9cc50716c 100644 --- a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx @@ -6,13 +6,15 @@ import _ from 'lodash' import classNames from 'classnames' import { useWindowSize, WindowSize } from '~/libs/shared' -import { Button, ConfirmModal, Table, TableColumn } from '~/libs/ui' +import { ConfirmModal, Table, TableColumn } from '~/libs/ui' import { IsRemovingType, MobileTableColumn, Submission } from '../../models' import { TableMobile } from '../common/TableMobile' import { TableWrapper } from '../common/TableWrapper' +import ShowHistoryButton from './ShowHistoryButton' import SubmissionTableActions from './SubmissionTableActions' +import SubmissionTableActionsNonMM from './SubmissionTableActionsNonMM' import styles from './SubmissionTable.module.scss' interface Props { @@ -26,11 +28,22 @@ interface Props { doPostBusEvent: (submissionId: string, testType: string) => void showSubmissionHistory: IsRemovingType setShowSubmissionHistory: Dispatch> + isMM: boolean + isDownloading: IsRemovingType + downloadSubmission: (submissionId: string) => void + isDoingAvScan: IsRemovingType + doPostBusEventAvScan: (submission: Submission) => void } export const SubmissionTable: FC = (props: Props) => { const { width: screenWidth }: WindowSize = useWindowSize() - const isTablet = useMemo(() => screenWidth <= 1479, [screenWidth]) + const isTablet = useMemo(() => { + if (props.isMM) { + return screenWidth <= 1479 + } + + return screenWidth <= 900 + }, [screenWidth, props.isMM]) const [ showConfirmDeleteSubmissionDialog, setShowConfirmDeleteSubmissionDialog, @@ -39,122 +52,167 @@ export const SubmissionTable: FC = (props: Props) => { = useState() const columns = useMemo[]>( - () => [ - { - label: 'Submitter', - propertyName: 'createdBy', - type: 'text', - }, - { - className: 'blockCellWrap', - label: 'ID', - propertyName: 'id', - type: 'text', - }, - { - label: 'Submission date', - propertyName: 'submittedDateString', - type: 'text', - }, - { - label: 'Example score', - renderer: (data: Submission) => ( - - {data.exampleScore === undefined - ? 'N/A' - : data.exampleScore} - - ), - type: 'element', - }, - { - label: 'Provisional score', - renderer: (data: Submission) => ( - - {data.provisionalScore === undefined - ? 'N/A' - : data.provisionalScore} - - ), - type: 'element', - }, - { - label: 'Final score', - renderer: (data: Submission) => ( - - {data.finalScore === undefined - ? 'N/A' - : data.finalScore} - - ), - type: 'element', - }, - { - label: 'Provisional rank', - renderer: (data: Submission) => ( - - {data.provisionalRank === undefined - ? 'N/A' - : data.provisionalRank} - - ), - type: 'element', - }, - { - label: 'Final rank', - renderer: (data: Submission) => ( - - {data.finalRank === undefined ? 'N/A' : data.finalRank} - - ), - type: 'element', - }, - { - label: '', - renderer: (data: Submission) => ( -
    - - {!data.hideToggleHistory && ( - - )} -
    - ), - type: 'element', - }, - ], + isDownloading={props.isDownloading} + downloadSubmission={function downloadSubmission() { + props.downloadSubmission(data.id) + }} + data={data} + /> + {!data.hideToggleHistory && ( + + )} +
    + ), + type: 'element', + }, + ]), // eslint-disable-next-line react-hooks/exhaustive-deps [ + props.isMM, props.isRemovingSubmission, props.isRemovingReviewSummations, props.isRunningTest, props.showSubmissionHistory, + props.isDownloading, + props.downloadSubmission, + props.isDoingAvScan, + props.doPostBusEventAvScan, ], ) diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.module.scss b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.module.scss new file mode 100644 index 000000000..628fcf8a0 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.module.scss @@ -0,0 +1,16 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + gap: 10px; + + @include ltelg { + flex-wrap: wrap; + } + + :global(button.btn-disabled.btn-style-primary) { + background-color: $turq-160 !important; + color: $tc-white !important; + opacity: 0.5; + } +} diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.tsx new file mode 100644 index 000000000..42ffc4f22 --- /dev/null +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActionsNonMM.tsx @@ -0,0 +1,45 @@ +/** + * Submission Table Actions For Non MM Challenge. + */ +import { FC } from 'react' + +import { Button } from '~/libs/ui' + +import { IsRemovingType, Submission } from '../../models' + +import styles from './SubmissionTableActionsNonMM.module.scss' + +interface Props { + data: Submission + isDownloading: IsRemovingType + downloadSubmission: () => void + isDoingAvScan: IsRemovingType + doPostBusEventAvScan: () => void +} + +export const SubmissionTableActionsNonMM: FC = (props: Props) => ( +
    + + {props.data.isTheLatestSubmission && ( + + )} +
    +) + +export default SubmissionTableActionsNonMM diff --git a/src/apps/admin/src/lib/hooks/index.ts b/src/apps/admin/src/lib/hooks/index.ts index 8f8ff2e7a..74d6e6aa0 100644 --- a/src/apps/admin/src/lib/hooks/index.ts +++ b/src/apps/admin/src/lib/hooks/index.ts @@ -30,3 +30,6 @@ export * from './useManageTerms' export * from './useManageAddTerm' export * from './useManageTermsUsers' export * from './useAutoScrollTopWhenInit' +export * from './useFetchChallenge' +export * from './useDownloadSubmission' +export * from './useManageAVScan' diff --git a/src/apps/admin/src/lib/hooks/useDownloadSubmission.ts b/src/apps/admin/src/lib/hooks/useDownloadSubmission.ts new file mode 100644 index 000000000..e5b131b31 --- /dev/null +++ b/src/apps/admin/src/lib/hooks/useDownloadSubmission.ts @@ -0,0 +1,62 @@ +/** + * Download submission + */ +import { useCallback, useMemo, useState } from 'react' +import { some } from 'lodash' + +import { downloadSubmissionFile } from '../services' +import { handleError } from '../utils' +import { IsRemovingType } from '../models' + +export interface useDownloadSubmissionProps { + isLoading: IsRemovingType + isLoadingBool: boolean + downloadSubmission: (submissionId: string) => void +} + +/** + * Download submission + * @returns download info + */ +export function useDownloadSubmission(): useDownloadSubmissionProps { + const [isLoading, setIsLoading] = useState({}) + const isLoadingBool = useMemo( + () => some(isLoading, value => value === true), + [isLoading], + ) + + const downloadSubmission = useCallback((submissionId: string) => { + setIsLoading(previous => ({ + ...previous, + [submissionId]: true, + })) + downloadSubmissionFile(submissionId) + .then((data: Blob) => { + setIsLoading(previous => ({ + ...previous, + [submissionId]: false, + })) + + const url = window.URL.createObjectURL(new Blob([data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `submission-${submissionId}.zip`) + document.body.appendChild(link) + link.click() + link.parentNode?.removeChild(link) + }) + .catch(e => { + setIsLoading(previous => ({ + ...previous, + [submissionId]: false, + })) + handleError(e) + }) + }, []) + + return { + downloadSubmission, + isLoading, + isLoadingBool, + } +} diff --git a/src/apps/admin/src/lib/hooks/useFetchChallenge.ts b/src/apps/admin/src/lib/hooks/useFetchChallenge.ts new file mode 100644 index 000000000..e5b9019a2 --- /dev/null +++ b/src/apps/admin/src/lib/hooks/useFetchChallenge.ts @@ -0,0 +1,51 @@ +/** + * Manage fetch challenge info + */ +import { useEffect, useRef, useState } from 'react' + +import { Challenge } from '../models' +import { getChallengeById } from '../services' +import { handleError } from '../utils' + +export interface useFetchChallengeProps { + isLoading: boolean + challengeInfo?: Challenge +} + +/** + * Fetch challenge info + * @returns challenge info + */ +export function useFetchChallenge( + challengeId: string, +): useFetchChallengeProps { + const [isLoading, setIsLoading] = useState(false) + + const isLoadingRef = useRef(false) + const [challengeInfo, setChallengeInfo] = useState() + + useEffect(() => { + if (challengeId && !isLoadingRef.current) { + isLoadingRef.current = true + setIsLoading(isLoadingRef.current) + setChallengeInfo(undefined) + + getChallengeById(challengeId) + .then((data: Challenge) => { + isLoadingRef.current = false + setIsLoading(isLoadingRef.current) + setChallengeInfo(data) + }) + .catch(e => { + isLoadingRef.current = false + setIsLoading(isLoadingRef.current) + handleError(e) + }) + } + }, [challengeId]) + + return { + challengeInfo, + isLoading, + } +} diff --git a/src/apps/admin/src/lib/hooks/useManageAVScan.ts b/src/apps/admin/src/lib/hooks/useManageAVScan.ts new file mode 100644 index 000000000..455000165 --- /dev/null +++ b/src/apps/admin/src/lib/hooks/useManageAVScan.ts @@ -0,0 +1,71 @@ +/** + * Manage bus event + */ +import { useCallback, useMemo, useState } from 'react' +import { toast } from 'react-toastify' +import _ from 'lodash' + +import { + CREATE_BUS_EVENT_AV_RESCAN, +} from '../../config/busEvent.config' +import { createAvScanSubmissionPayload, reqToBusAPI } from '../services' +import { handleError } from '../utils' +import { IsRemovingType, Submission } from '../models' + +export interface useManageAVScanProps { + isLoading: IsRemovingType + isLoadingBool: boolean + doPostBusEvent: (submission: Submission) => void +} + +/** + * Manage bus event + */ +export function useManageAVScan(): useManageAVScanProps { + const [isLoading, setIsLoading] = useState({}) + const isLoadingBool = useMemo( + () => _.some(isLoading, value => value === true), + [isLoading], + ) + + const doPostBusEvent = useCallback((submission: Submission) => { + setIsLoading(previous => ({ + ...previous, + [submission.id]: true, + })) + + function cbError(e: Error): void { + setIsLoading(previous => ({ + ...previous, + [submission.id]: false, + })) + handleError(e) + } + + createAvScanSubmissionPayload(submission) + .then(payload => { + const data = CREATE_BUS_EVENT_AV_RESCAN(payload) + reqToBusAPI(data) + .then(() => { + setIsLoading(previous => ({ + ...previous, + [submission.id]: false, + })) + toast.success( + 'Sending request to av rescan successfully', + { + toastId: 'Av scan', + }, + ) + }) + .catch(cbError) + }) + .catch(cbError) + }, []) + + return { + doPostBusEvent, + isLoading, + isLoadingBool, + } +} diff --git a/src/apps/admin/src/lib/models/CommonRequestBusAPI.type.ts b/src/apps/admin/src/lib/models/CommonRequestBusAPI.type.ts new file mode 100644 index 000000000..905bb7b70 --- /dev/null +++ b/src/apps/admin/src/lib/models/CommonRequestBusAPI.type.ts @@ -0,0 +1,7 @@ +import { RequestBusAPI } from './RequestBusAPI.model' +import { RequestBusAPIAVScan } from './RequestBusAPIAVScan.model' + +/** + * Common type for bus api request + */ +export type CommonRequestBusAPI = RequestBusAPI | RequestBusAPIAVScan diff --git a/src/apps/admin/src/lib/models/RequestBusAPIAVScan.model.ts b/src/apps/admin/src/lib/models/RequestBusAPIAVScan.model.ts new file mode 100644 index 000000000..c5a225e71 --- /dev/null +++ b/src/apps/admin/src/lib/models/RequestBusAPIAVScan.model.ts @@ -0,0 +1,21 @@ +/** + * Request to av scan bus api + */ +export interface RequestBusAPIAVScanPayload { + submissionId: string + url: string + fileName?: string + moveFile: boolean + cleanDestinationBucket: string + quarantineDestinationBucket: string + callbackOption: string + callbackKafkaTopic: string +} + +export interface RequestBusAPIAVScan { + topic: string + originator: string + timestamp: string + 'mime-type': string + payload: RequestBusAPIAVScanPayload +} diff --git a/src/apps/admin/src/lib/models/ValidateS3URIResult.model.ts b/src/apps/admin/src/lib/models/ValidateS3URIResult.model.ts new file mode 100644 index 000000000..29bfcfbd2 --- /dev/null +++ b/src/apps/admin/src/lib/models/ValidateS3URIResult.model.ts @@ -0,0 +1,8 @@ +/** + * Validate s3 url result + */ +export interface ValidateS3URIResult { + isValid: boolean + bucket?: string + key?: string +} diff --git a/src/apps/admin/src/lib/models/index.ts b/src/apps/admin/src/lib/models/index.ts index 926c74ba5..7ad54810d 100644 --- a/src/apps/admin/src/lib/models/index.ts +++ b/src/apps/admin/src/lib/models/index.ts @@ -32,6 +32,7 @@ export * from './FormGroupMembersFilters.model' export * from './RoleMemberInfo.model' export * from './Submission.model' export * from './RequestBusAPI.model' +export * from './RequestBusAPIAVScan.model' export * from './MemberSubmission.model' export * from './SSOUserLogin.model' export * from './SSOLoginProvider.model' @@ -41,8 +42,10 @@ export * from './TermAgreeabilityType.model' export * from './FormTermsUsersFilter.model' export * from './MemberInfo.model' export * from './TermUserInfo.model' +export * from './ValidateS3URIResult.model' export * from './FormAddGroupMembers.type' export * from './TableFilterType.type' export * from './TableRolesFilter.type' export * from './AdminAppContextType.type' export * from './IsRemoving.type' +export * from './CommonRequestBusAPI.type' diff --git a/src/apps/admin/src/lib/services/bus-event.service.ts b/src/apps/admin/src/lib/services/bus-event.service.ts index 82a352944..082bdb110 100644 --- a/src/apps/admin/src/lib/services/bus-event.service.ts +++ b/src/apps/admin/src/lib/services/bus-event.service.ts @@ -4,15 +4,15 @@ import { EnvironmentConfig } from '~/config' import { xhrPostAsync } from '~/libs/core' -import { RequestBusAPI } from '../models' +import { CommonRequestBusAPI } from '../models' /** * Send post event to bus api * @param data bus event data * @returns resolve to empty string if success */ -export const reqToBusAPI = async (data: RequestBusAPI): Promise => { - const resultData = await xhrPostAsync( +export const reqToBusAPI = async (data: CommonRequestBusAPI): Promise => { + const resultData = await xhrPostAsync( `${EnvironmentConfig.API.V5}/bus/events`, data, ) diff --git a/src/apps/admin/src/lib/services/submissions.service.ts b/src/apps/admin/src/lib/services/submissions.service.ts index 9bec8b654..be09c2e87 100644 --- a/src/apps/admin/src/lib/services/submissions.service.ts +++ b/src/apps/admin/src/lib/services/submissions.service.ts @@ -2,14 +2,17 @@ * Submissions service */ import { EnvironmentConfig } from '~/config' -import { xhrDeleteAsync, xhrGetAsync } from '~/libs/core' +import { xhrDeleteAsync, xhrGetAsync, xhrGetBlobAsync } from '~/libs/core' import { adjustSubmissionsResponse, ApiV5ResponseSuccess, MemberSubmission, + RequestBusAPIAVScanPayload, Submission, + ValidateS3URIResult, } from '../models' +import { validateS3URI } from '../utils' /** * Gets all submissions of challenge @@ -42,3 +45,44 @@ export const removeSubmission = async ( ) return result } + +/** + * Download submission file + * @param submissionId submission id + * @returns resolves to the submission file + */ +export const downloadSubmissionFile = async ( + submissionId: string, +): Promise => { + const results = await xhrGetBlobAsync( + `${EnvironmentConfig.API.V5}/submissions/${submissionId}/download`, + ) + return results +} + +/** + * Create av scan submission payload + * @param submissionInfo submission info + * @returns resolves to the av scan submission payload + */ +export const createAvScanSubmissionPayload = async ( + submissionInfo: Submission, +): Promise => { + const url = submissionInfo.url + const { isValid, key: fileName }: ValidateS3URIResult = validateS3URI(url) + if (!isValid) { + throw new Error('Submission url is not a valid') + } + + return { + callbackKafkaTopic: EnvironmentConfig.ADMIN.SUBMISSION_SCAN_TOPIC, + callbackOption: 'kafka', + cleanDestinationBucket: EnvironmentConfig.ADMIN.AWS_CLEAN_BUCKET, + fileName, + moveFile: true, + quarantineDestinationBucket: + EnvironmentConfig.ADMIN.AWS_QUARANTINE_BUCKET, + submissionId: submissionInfo.id, + url, + } +} diff --git a/src/apps/admin/src/lib/utils/challenge.ts b/src/apps/admin/src/lib/utils/challenge.ts index 13007f1be..e800ad1f1 100644 --- a/src/apps/admin/src/lib/utils/challenge.ts +++ b/src/apps/admin/src/lib/utils/challenge.ts @@ -11,7 +11,7 @@ import { Challenge, MemberSubmission } from '../models' * @param challenge challenge info * @returns true if challenge is mm */ -export function checkIsMM(challenge: Challenge): boolean { +export function checkIsMM(challenge?: Challenge): boolean { const tags = _.get(challenge, 'tags') || [] const isMMType = challenge ? challenge.type === 'Marathon Match' : false return tags.includes('Marathon Match') || isMMType diff --git a/src/apps/admin/src/lib/utils/others.ts b/src/apps/admin/src/lib/utils/others.ts index 2d8cbfa1b..0cac22b99 100644 --- a/src/apps/admin/src/lib/utils/others.ts +++ b/src/apps/admin/src/lib/utils/others.ts @@ -1,6 +1,11 @@ /** * Util for other check */ +import AmazonS3URI from 'amazon-s3-uri' + +import { EnvironmentConfig } from '~/config' + +import { ValidateS3URIResult } from '../models' /** * Check if object is date @@ -19,3 +24,30 @@ export function checkIsDateObject(date: any): boolean { export function checkIsNumberObject(numberObject: any): boolean { return typeof numberObject === 'number' } + +/** + * Validate s3 url + * @param fileURL file url + * @returns resolve to validate result + */ +export function validateS3URI( + fileURL: string, +): ValidateS3URIResult { + try { + const { region, bucket, key }: AmazonS3URI = AmazonS3URI(fileURL) + if ( + region !== EnvironmentConfig.ADMIN.AWS_REGION + || bucket !== EnvironmentConfig.ADMIN.AWS_DMZ_BUCKET + ) { + return { isValid: false } + } + + return { + bucket: bucket ?? undefined, + isValid: true, + key: key ?? undefined, + } + } catch (error) {} + + return { isValid: false } +} diff --git a/src/config/environments/default.env.ts b/src/config/environments/default.env.ts index 7c48c826f..193b13975 100644 --- a/src/config/environments/default.env.ts +++ b/src/config/environments/default.env.ts @@ -80,10 +80,16 @@ export const ADMIN = { AGREE_ELECTRONICALLY: '5b2798b2-ae82-4210-9b4d-5d6428125ccb', AGREE_FOR_DOCUSIGN_TEMPLATE: '999a26ad-b334-453c-8425-165d4cf496d7', AV_SCAN_SCORER_REVIEW_TYPE_ID: '68c5a381-c8ab-48af-92a7-7a869a4ee6c3', + AVSCAN_TOPIC: 'avscan.action.scan', + AWS_CLEAN_BUCKET: '', + AWS_DMZ_BUCKET: 'topcoder-dev-submissions', + AWS_QUARANTINE_BUCKET: '', + AWS_REGION: 'us-east-1', CHALLENGE_URL: 'https://www.topcoder-dev.com/challenges', CONNECT_URL: 'https://connect.topcoder-dev.com', DEFAULT_PAYMENT_TERMS: 1, DIRECT_URL: 'https://www.topcoder-dev.com/direct', ONLINE_REVIEW_URL: 'https://software.topcoder-dev.com/review', + SUBMISSION_SCAN_TOPIC: 'submission.scan.complete', WORK_MANAGER_URL: 'https://challenges.topcoder-dev.com', } diff --git a/src/config/environments/global-config.model.ts b/src/config/environments/global-config.model.ts index 0a813b77a..abaf3c391 100644 --- a/src/config/environments/global-config.model.ts +++ b/src/config/environments/global-config.model.ts @@ -53,5 +53,11 @@ export interface GlobalConfig { AV_SCAN_SCORER_REVIEW_TYPE_ID: string AGREE_ELECTRONICALLY: string AGREE_FOR_DOCUSIGN_TEMPLATE: string + AWS_REGION: string + AWS_DMZ_BUCKET: string + AWS_CLEAN_BUCKET: string + AWS_QUARANTINE_BUCKET: string + SUBMISSION_SCAN_TOPIC: string + AVSCAN_TOPIC: string } } diff --git a/src/config/environments/prod.env.ts b/src/config/environments/prod.env.ts index f0938d0a3..5fe1b34bb 100644 --- a/src/config/environments/prod.env.ts +++ b/src/config/environments/prod.env.ts @@ -10,10 +10,16 @@ export const ADMIN = { AGREE_ELECTRONICALLY: '2db6c920-4089-4755-9cd1-99b0df0af961', AGREE_FOR_DOCUSIGN_TEMPLATE: '1363a7ab-fd3e-4d7c-abbb-2f7440b8b355', AV_SCAN_SCORER_REVIEW_TYPE_ID: '55bbb17d-aac2-45a6-89c3-a8d102863d05', + AVSCAN_TOPIC: 'avscan.action.scan', + AWS_CLEAN_BUCKET: '', + AWS_DMZ_BUCKET: 'topcoder-submissions', + AWS_QUARANTINE_BUCKET: '', + AWS_REGION: 'us-east-1', CHALLENGE_URL: 'https://www.topcoder.com/challenges', CONNECT_URL: 'https://connect.topcoder.com', DEFAULT_PAYMENT_TERMS: 1, DIRECT_URL: 'https://www.topcoder.com/direct', ONLINE_REVIEW_URL: 'https://software.topcoder.com/review', + SUBMISSION_SCAN_TOPIC: 'submission.scan.complete', WORK_MANAGER_URL: 'https://challenges.topcoder.com', } diff --git a/yarn.lock b/yarn.lock index b4e8f1d84..e513893b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6013,6 +6013,11 @@ ajv@^8.9.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +amazon-s3-uri@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/amazon-s3-uri/-/amazon-s3-uri-0.1.1.tgz#37afdfa88352ee0c22ac12ea6417a7c725f2b90b" + integrity sha512-LklZtJ3lgTFdVpy/5ln0okxdgMdnRmFLRg9FGcJ7DeB5Ez5TCs1DHdmVovcPIxW9tQlA1+QLpGNg1Ig6hv768A== + ansi-align@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"