diff --git a/.envexample b/.envexample index c00acf00..a70b7c20 100644 --- a/.envexample +++ b/.envexample @@ -15,4 +15,4 @@ FUSIONAUTH_URL: http://fusionauth:9011 FUSIONAUTH_APPLICATION_ID: 23e4b229-1219-42e5-aed6-f9b6f1eedef8 # Frontend -API_BASE_URL=http://proxy/cpf/api +NEXT_PUBLIC_API_BASE_URL: http://proxy/cpf/api diff --git a/.github/workflows/trivy-frontend.yml b/.github/workflows/trivy-frontend.yml new file mode 100644 index 00000000..2fec8ca1 --- /dev/null +++ b/.github/workflows/trivy-frontend.yml @@ -0,0 +1,55 @@ +name: Scan Frontend with Trivy + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + branches: + - main + - develop + +env: + IMAGE_NAME: frontend + VERSION: v1 + +jobs: + build_docker_image: + name: Build docker image + timeout-minutes: 15 + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Build docker image + run: docker build . --file Dockerfile --tag $IMAGE_NAME + + - name: Log in to gHRC + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Push image + run: | + docker tag $IMAGE_NAME ghcr.io/tivix/cpf/$IMAGE_NAME:$VERSION + docker push ghcr.io/tivix/cpf/$IMAGE_NAME:$VERSION + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.23.0 + with: + image-ref: "ghcr.io/tivix/cpf/${{ env.IMAGE_NAME }}:${{ env.VERSION }}" + scanners: "vuln,secret,config" + format: "sarif" + output: "trivy-fe-results.sarif" + severity: "CRITICAL,HIGH" + + - name: Upload scan result to Github Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-fe-results.sarif + category: "image" diff --git a/frontend/src/api/bucket.ts b/frontend/src/api/bucket.ts new file mode 100644 index 00000000..1068b904 --- /dev/null +++ b/frontend/src/api/bucket.ts @@ -0,0 +1,16 @@ +import { mapKeysToCamelCase } from '@app/utils'; +import { API_URLS } from '.'; +import { Bucket } from '@app/types/common'; + +async function getBucketDetails(slug: string) { + const response = await fetch(`${API_URLS.library.buckets}/${slug}`); + + if (!response.ok) { + throw new Error('Failed to fetch bucket details'); + } + const data = await response.json(); + + return mapKeysToCamelCase(data); +} + +export { getBucketDetails }; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 03a3ad94..43434677 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,4 +1,4 @@ -const baseUrl = process.env.API_BASE_URL; +const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; export const API_URLS = { library: { diff --git a/frontend/src/api/ladder.ts b/frontend/src/api/ladder.ts new file mode 100644 index 00000000..3445bf7d --- /dev/null +++ b/frontend/src/api/ladder.ts @@ -0,0 +1,45 @@ +import { mapKeysToCamelCase } from '@app/utils'; +import { API_URLS } from '.'; +import { LadderCardInterface } from '@app/components/common/LadderCard'; +import { LadderBand } from '@app/types/common'; + +async function getLadders() { + const response = await fetch(API_URLS.library.ladders); + + if (!response.ok) { + throw new Error('Failed to fetch ladders'); + } + const data = await response.json(); + + return mapKeysToCamelCase(data); +} + +async function getLadderDetails(slug: string) { + const response = await fetch(`${API_URLS.library.ladders}/${slug}`); + + if (!response.ok) { + throw new Error('Failed to fetch ladder details'); + } + const data = await response.json(); + + return mapKeysToCamelCase<{ + ladderName: string; + bands: Record; + }>(data); +} + +async function getLadderName(slug: string) { + const response = await fetch(`${API_URLS.library.ladders}/${slug}`); + + if (!response.ok) { + throw new Error('Failed to fetch ladder details'); + } + const data = await response.json(); + + return mapKeysToCamelCase<{ + ladderName: string; + bands: Record; + }>(data).ladderName; +} + +export { getLadders, getLadderDetails, getLadderName }; diff --git a/frontend/src/app/(app)/(root)/library/[ladder]/[bucket]/page.tsx b/frontend/src/app/(app)/(root)/library/[ladder]/[bucket]/page.tsx index a9c47bb2..fa7254b0 100644 --- a/frontend/src/app/(app)/(root)/library/[ladder]/[bucket]/page.tsx +++ b/frontend/src/app/(app)/(root)/library/[ladder]/[bucket]/page.tsx @@ -1,35 +1,9 @@ import { Breadcrumbs } from '@app/components/modules/Breadcrumbs'; -import { mapKeysToCamelCase } from '@app/utils'; import { BucketDetails } from '@app/components/modules/BucketDetails'; -import { API_URLS } from '@app/api'; -import { Bucket, LadderBand } from '@app/types/common'; +import { getLadderName } from '@app/api/ladder'; +import { getBucketDetails } from '@app/api/bucket'; import { routes } from '@app/constants'; -async function getBucketDetails(slug: string) { - const response = await fetch(`${API_URLS.library.buckets}/${slug}`); - - if (!response.ok) { - throw new Error('Failed to fetch bucket details'); - } - const data = await response.json(); - - return mapKeysToCamelCase(data); -} - -async function getLadderName(slug: string) { - const response = await fetch(`${API_URLS.library.ladders}/${slug}`); - - if (!response.ok) { - throw new Error('Failed to fetch ladder details'); - } - const data = await response.json(); - - return mapKeysToCamelCase<{ - ladderName: string; - bands: Record; - }>(data).ladderName; -} - export default async function BucketDetailed({ params }: { params: { bucket: string; ladder: string } }) { const { bucket, ladder } = params; const data = await getBucketDetails(bucket); @@ -48,3 +22,5 @@ export default async function BucketDetailed({ params }: { params: { bucket: str ); } + +export const dynamic = 'force-dynamic'; diff --git a/frontend/src/app/(app)/(root)/library/[ladder]/page.tsx b/frontend/src/app/(app)/(root)/library/[ladder]/page.tsx index f72043fe..662c5f5b 100644 --- a/frontend/src/app/(app)/(root)/library/[ladder]/page.tsx +++ b/frontend/src/app/(app)/(root)/library/[ladder]/page.tsx @@ -1,24 +1,8 @@ import { Breadcrumbs } from '@app/components/modules/Breadcrumbs'; import { LibraryDetailed } from '@app/components/modules/LibraryDetailed'; -import { mapKeysToCamelCase } from '@app/utils'; -import { API_URLS } from '@app/api'; -import { LadderBand } from '@app/types/common'; +import { getLadderDetails } from '@app/api/ladder'; import { routes } from '@app/constants'; -async function getLadderDetails(slug: string) { - const response = await fetch(`${API_URLS.library.ladders}/${slug}`); - - if (!response.ok) { - throw new Error('Failed to fetch ladder details'); - } - const data = await response.json(); - - return mapKeysToCamelCase<{ - ladderName: string; - bands: Record; - }>(data); -} - export default async function LadderDetailed({ params }: { params: { ladder: string } }) { const data = await getLadderDetails(params.ladder); @@ -34,3 +18,5 @@ export default async function LadderDetailed({ params }: { params: { ladder: str ); } + +export const dynamic = 'force-dynamic'; diff --git a/frontend/src/app/(app)/(root)/library/page.tsx b/frontend/src/app/(app)/(root)/library/page.tsx index 3193f3c5..01de50c2 100644 --- a/frontend/src/app/(app)/(root)/library/page.tsx +++ b/frontend/src/app/(app)/(root)/library/page.tsx @@ -1,17 +1,5 @@ -import { mapKeysToCamelCase } from '@app/utils'; -import { LadderCard, LadderCardInterface } from '@app/components/common/LadderCard'; -import { API_URLS } from '@app/api'; - -async function getLadders() { - const response = await fetch(API_URLS.library.ladders); - - if (!response.ok) { - throw new Error('Failed to fetch ladders'); - } - const data = await response.json(); - - return mapKeysToCamelCase(data); -} +import { LadderCard } from '@app/components/common/LadderCard'; +import { getLadders } from '@app/api/ladder'; export default async function LibraryPage() { const data = await getLadders(); @@ -21,10 +9,12 @@ export default async function LibraryPage() {

CPF Library

Select a career path to view the details.

- {data.map((ladder: LadderCardInterface) => ( + {data.map((ladder) => ( ))}
); } + +export const dynamic = 'force-dynamic'; diff --git a/frontend/src/static/icons/ChevronDoubleLeftIcon.tsx b/frontend/src/static/icons/ChevronDoubleLeftIcon.tsx index 6c83705b..d44a3411 100644 --- a/frontend/src/static/icons/ChevronDoubleLeftIcon.tsx +++ b/frontend/src/static/icons/ChevronDoubleLeftIcon.tsx @@ -3,9 +3,9 @@ export const ChevronDoubleLeftIcon: React.FC> = (p ); diff --git a/frontend/src/static/icons/CloseIcon.tsx b/frontend/src/static/icons/CloseIcon.tsx index 8bcf1dd1..0bfaeb4f 100644 --- a/frontend/src/static/icons/CloseIcon.tsx +++ b/frontend/src/static/icons/CloseIcon.tsx @@ -1,5 +1,5 @@ export const CloseIcon: React.FC> = (props) => ( - + );