Skip to content

Commit

Permalink
Merge branch 'develop' into feat/GAP-2420-admin
Browse files Browse the repository at this point in the history
  • Loading branch information
jgunnCO committed Apr 5, 2024
2 parents 0952bc1 + 229689d commit 3222fc1
Show file tree
Hide file tree
Showing 229 changed files with 9,798 additions and 2,470 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/admin-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- feat/**
- feature/**
paths:
- "packages/admin/**"
- "packages/gap-web-ui/**"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/applicant-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- feat/**
- feature/**
paths:
- "packages/applicant/**"
- "packages/gap-web-ui/**"
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/gap-web-ui-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- feat/**
- feature/**
paths:
- "packages/gap-web-ui/**"
- ".github/workflows/gap-web-ui-ci.yml"
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.17.0
18.19.1
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ARG NODE_VERSION=18.17.0
FROM --platform=linux/amd64 node:${NODE_VERSION}-alpine as build
ARG IMAGE_NAME=18.19-alpine
FROM --platform=linux/amd64 node:${IMAGE_NAME} as build

ARG APP_NAME

Expand All @@ -20,7 +20,7 @@ RUN yarn workspace gap-web-ui build

RUN yarn workspace ${APP_NAME} build

FROM --platform=linux/amd64 node:${NODE_VERSION}-alpine
FROM --platform=linux/amd64 node:${IMAGE_NAME}

ARG APP_NAME

Expand Down
37 changes: 22 additions & 15 deletions packages/admin/.env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
SUB_PATH=/apply/admin
APPLICANT_DOMAIN=https://localhost:3000/apply/applicant
MAX_COOKIE_AGE=21600
BACKEND_HOST=http://localhost:8080
COOKIE_SECRET=E6AAAB815903D8CDD81246EFC8275C13EE34541134092A635572C8F7F47448BE
CYPRESS_DATABASE_URL=postgres://postgres:postgres@localhost:5432
CYPRESS_USER_SERVICE_DB_NAME=gapuserlocaldb
CYPRESS_WIREMOCK_BASE_URL=http://localhost:8888/__admin
ENCRYPTION_GENERATOR_KEY=encryption-generator-key
ENCRYPTION_KEY_NAME=encryption-key-name
ENCRYPTION_KEY_NAMESPACE=encryption-key-namespace
ENCRYPTION_ORIGIN=eu-west-2
ENCRYPTION_STAGE=encryption-stage
ENCRYPTION_WRAPPING_KEY=encryption-wrapping-key
FEATURE_ADVERT_BUILDER=enabled
FIND_A_GRANT_URL=http://localhost:3000
HOST=http://localhost:3000/apply/admin
JWT_COOKIE_NAME=user-service-token
LOGIN_URL=http://localhost:8082/login?redirectUrl=http://localhost:3001/apply/admin
LOGOUT_URL=http://localhost:8082/logout
MAX_COOKIE_AGE=21600
ONE_LOGIN_ENABLED=false
ONE_LOGIN_MIGRATION_JOURNEY_ENABLED=false
SESSION_COOKIE_NAME=session_id
BACKEND_HOST=http://localhost:8080
USER_SERVICE_URL=http://localhost:8082
COOKIE_SECRET=E6AAAB815903D8CDD81246EFC8275C13EE34541134092A635572C8F7F47448BE
SPOTLIGHT_URL=https://cabinetoffice-spotlight.force.com/s/login/
FEATURE_ADVERT_BUILDER=enabled
ONE_LOGIN_ENABLED=false
V2_LOGIN_URL=http://localhost:8082/v2/login?redirectUrl=http://localhost:3001/apply/admin
SUB_PATH=/apply/admin
SUPER_ADMIN_DASHBOARD_URL=http://localhost:3001/apply/admin/super-admin-dashboard
USER_SERVICE_URL=http://localhost:8082
V2_LOGIN_URL=http://localhost:8082/v2/login
V2_LOGOUT_URL=http://localhost:8082/v2/logout
FIND_A_GRANT_URL=http://localhost:3000
ONE_LOGIN_MIGRATION_JOURNEY_ENABLED=false
CYPRESS_WIREMOCK_BASE_URL=http://localhost:8888/__admin
CYPRESS_DATABASE_URL=postgres://postgres:postgres@localhost:5432
CYPRESS_USER_SERVICE_DB_NAME=gapuserlocaldb
VALIDATE_USER_ROLES_IN_MIDDLEWARE=true
SUPER_ADMIN_DASHBOARD_URL=http://localhost:3001/apply/admin/super-admin-dashboard
VALIDATE_USER_ROLES_IN_MIDDLEWARE=true
Binary file modified packages/admin/public/assets/images/favicon.ico
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/admin/public/assets/images/govuk-apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 1 addition & 7 deletions packages/admin/public/assets/images/govuk-mask-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/admin/public/assets/images/govuk-opengraph-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/admin/public/favicon.ico
Binary file not shown.
23 changes: 23 additions & 0 deletions packages/admin/src/components/layout/Header.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import Header from './Header';
import { useAdminAuth } from '../../pages/_app.page';

jest.mock('../../pages/_app.page');

const mockUseAdminAuth = jest.mocked(useAdminAuth);

describe('Testing Header component', () => {
it('Renders a sign out button', () => {
mockUseAdminAuth.mockReturnValue({ isSuperAdmin: false });
render(<Header />);
screen.getByRole('link', { name: 'Sign out' });
});

it('should render Beta block', () => {
mockUseAdminAuth.mockReturnValue({ isSuperAdmin: false });
render(<Header />);
screen.getByText(/beta/i);
expect(
Expand All @@ -21,3 +28,19 @@ describe('Testing Header component', () => {
);
});
});

describe('Testing SuperAdmin Dashboard link', () => {
it('It should render SuperAdmin Dashboard link', () => {
mockUseAdminAuth.mockReturnValue({ isSuperAdmin: true });
render(<Header />);
expect(screen.getByText('Superadmin Dashboard')).toBeVisible();
expect(
screen.getByRole('link', { name: 'Superadmin Dashboard' })
).toBeInTheDocument();
});
it('It should NOT render SuperAdmin Dashboard link', () => {
mockUseAdminAuth.mockReturnValue({ isSuperAdmin: false });
render(<Header />);
expect(screen.queryByText('Superdmin Dashboard')).toBeNull();
});
});
27 changes: 20 additions & 7 deletions packages/admin/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Image from 'next/image';
import { isIE } from 'react-device-detect';
import getConfig from 'next/config';
import CustomLink from '../custom-link/CustomLink';
import { useAdminAuth } from '../../pages/_app.page';

const Header = () => {
const feedbackContent = `https://docs.google.com/forms/d/e/1FAIpQLSd2V0IqOMpb2_yQnz_Ges0WCYFnDOTxZpF299gePV1j8kMdLA/viewform`;
const { publicRuntimeConfig } = getConfig();
const { isSuperAdmin } = useAdminAuth();
return (
<>
<header className="govuk-header" role="banner" data-module="govuk-header">
Expand Down Expand Up @@ -61,16 +63,27 @@ const Header = () => {
</a>
</div>
<div className="govuk-header__content">
<a
href={`${publicRuntimeConfig.SUB_PATH}/dashboard`}
className="govuk-header__link govuk-header__link--service-name"
>
Manage a grant
</a>
<div className="govuk-header__content">
<a
href={`${publicRuntimeConfig.SUB_PATH}/dashboard`}
className="govuk-header__link govuk-header__link--service-name"
>
Manage a grant
</a>
</div>
{isSuperAdmin && (
<div className="super-admin-link govuk-!-padding-top-2">
<a
href={`${publicRuntimeConfig.SUB_PATH}/super-admin-dashboard`}
className="govuk-header__link govuk-!-margin-left-9 govuk-!-font-weight-bold"
>
Superadmin Dashboard
</a>
</div>
)}
</div>
</div>
</header>

<nav>
<div className="govuk-width-container">
<div className="govuk-phase-banner">
Expand Down
5 changes: 4 additions & 1 deletion packages/admin/src/components/pagination/Pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const Pagination = ({
itemsPerPage = 10,
totalItems = 0,
itemType = 'items',
itemCountMargin = false,
}) => {
const router = useRouter();

Expand Down Expand Up @@ -110,7 +111,9 @@ const Pagination = ({
</>
)}
<p
className="moj-pagination__results"
className={`moj-pagination__results ${
itemCountMargin ? 'govuk-!-margin-top-9' : ''
}`}
data-cy="cyPaginationShowingGrants"
style={{
paddingTop: '0.6rem',
Expand Down
1 change: 1 addition & 0 deletions packages/admin/src/enums/ExportStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum ExportStatusEnum {
ERROR = 'ERROR',
REQUESTED = 'REQUESTED',
EXPIRED = 'EXPIRED',
FAILED = 'FAILED',
}

export default ExportStatusEnum;
15 changes: 12 additions & 3 deletions packages/admin/src/middleware.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,16 @@ const authenticateRequest = async (req: NextRequest, res: NextResponse) => {
});

res.headers.set('Cache-Control', 'no-store');

return res;
} else {
return NextResponse.redirect(getLoginUrl());
}
let url = getLoginUrl();
console.log('Middleware redirect URL: ' + url);
if (submissionDownloadPattern.test({ pathname: req.nextUrl.pathname })) {
url = `${url}?redirectUrl=${process.env.HOST}${req.nextUrl.pathname}`;
console.log('Getting submission export download redirect URL: ' + url);
}
console.log('Final redirect URL from admin middleware: ' + url);
return NextResponse.redirect(url);
};

const httpLoggers = {
Expand Down Expand Up @@ -106,3 +111,7 @@ export async function middleware(req: NextRequest) {
logResponse(req, res);
return res;
}

const submissionDownloadPattern = new URLPattern({
pathname: '/scheme/:schemeId([0-9]+)/:exportBatchUuid([0-9a-f-]+)',
});
16 changes: 10 additions & 6 deletions packages/admin/src/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import '@testing-library/jest-dom';
import { middleware } from './middleware.page';
// eslint-disable-next-line @next/next/no-server-import-in-page
import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies';
// eslint-disable-next-line @next/next/no-server-import-in-page
import { NextRequest, NextResponse } from 'next/server';
import { middleware } from './middleware.page';
import { isAdminSessionValid } from './services/UserService';
import { getLoginUrl } from './utils/general';
import {
RequestCookie,
ResponseCookie,
} from 'next/dist/compiled/@edge-runtime/cookies';

jest.mock('./utils/csrfMiddleware');
jest.mock('./utils/general');
jest.mock('./services/UserService', () => ({
isAdminSessionValid: jest.fn(),
}));

jest.mock('next/server', () => ({
...jest.requireActual('next/server'),
URLPattern: jest.fn().mockImplementation(() => ({
test: jest.fn(),
})),
}));

describe('middleware', () => {
const req = new NextRequest('http://localhost:3000/dashboard');

Expand Down
29 changes: 23 additions & 6 deletions packages/admin/src/pages/_app.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ import App from 'next/app';
import getConfig from 'next/config';
import Script from 'next/script';
import nookies from 'nookies';
import { useEffect } from 'react';
import { createContext, useContext, useEffect } from 'react';
import TagManager from 'react-gtm-module';
import '../../../../node_modules/gap-web-ui/dist/cjs/index.css';
import Layout from '../components/layout/Layout';
import '../lib/ie11_nodelist_polyfill';
import '../styles/globals.scss';
import { getUserRoles } from '../services/UserService';

const MyApp = ({ Component, pageProps, cookies }) => {
const USER_TOKEN_NAME = process.env.JWT_COOKIE_NAME;
export const AuthContext = createContext({
isSuperAdmin: false,
});
export const useAdminAuth = () => useContext(AuthContext);

const MyApp = ({ Component, pageProps, cookies, isSuperAdmin }) => {
const { publicRuntimeConfig } = getConfig();

const showCookieBanner = !cookies.design_system_cookies_policy;
Expand Down Expand Up @@ -40,20 +47,30 @@ const MyApp = ({ Component, pageProps, cookies }) => {
src={`${publicRuntimeConfig.SUB_PATH}/javascript/govuk.js`}
strategy="beforeInteractive"
/>
<Layout showCookieBanner={showCookieBanner}>
<Component {...pageProps} />
</Layout>
<AuthContext.Provider value={{ isSuperAdmin }}>
<Layout showCookieBanner={showCookieBanner}>
<Component {...pageProps} />
</Layout>
</AuthContext.Provider>
</>
);
};

MyApp.getInitialProps = async (appContext) => {
const appProps = await App.getInitialProps(appContext);
const { req } = appContext.ctx;
const userServiceToken = req.cookies[USER_TOKEN_NAME];
const cookies =
typeof window === 'undefined'
? appContext.ctx.req.cookies
: nookies.get({});
return { ...appProps, cookies };

try {
const isSuperAdmin = (await getUserRoles(userServiceToken)).isSuperAdmin;
return { ...appProps, cookies, isSuperAdmin };
} catch (e) {
return { ...appProps, cookies, isSuperAdmin: false };
}
};

export default MyApp;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import {
downloadSummary,
getApplicationFormSummary,
} from '../../../../services/ApplicationService';
import { getSessionIdFromCookies } from '../../../../utils/session';
import { APIGlobalHandler } from '../../../../utils/apiErrorHandler';

async function handler(req: NextApiRequest, res: NextApiResponse) {
const sessionCookie = getSessionIdFromCookies(req);
const applicationId = (req.query.applicationId || '').toString();

const { data } = await downloadSummary(applicationId, sessionCookie);

const application = await getApplicationFormSummary(
applicationId,
sessionCookie,
false,
false
);
const applicationName = application.applicationName
.substring(0, 100)
.replaceAll(new RegExp('[^a-zA-Z0-9\\-\\.()]', 'g'), '_');
const filename = `${applicationName}_questions.odt`;

res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);
res.send(Buffer.from(data));
}

export default (req: NextApiRequest, res: NextApiResponse) =>
APIGlobalHandler(req, res, handler);
Loading

0 comments on commit 3222fc1

Please sign in to comment.