Skip to content

Commit

Permalink
feat: Implement keycloak protection for /admin endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
raarielgrace committed Dec 17, 2024
1 parent ef4325f commit af1b4c7
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 51 deletions.
7 changes: 7 additions & 0 deletions backend/src/controllers/admin-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Response, Request } from 'express';

const getBlah = async (req: Request, res: Response) => {
res.status(200).send('Blah blah blah');
};

export default getBlah;
5 changes: 2 additions & 3 deletions backend/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import morgan from 'morgan';
import cors from 'cors';
import swaggerUi from 'swagger-ui-express';
import swaggerJSDoc from 'swagger-jsdoc';
import { sso } from '@bcgov/citz-imb-sso-express';
import { protectedRoute, sso } from '@bcgov/citz-imb-sso-express';
import swaggerConfig from './config/swaggerConfig';
import * as routers from './routes/index';
import * as middleware from './middleware';
Expand All @@ -31,10 +31,9 @@ app.use(morgan('dev')); // Logger Requests and Responses in the console
app.use(cors()); // Activate CORS, allowing access
app.use('/api/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerJSDoc(swaggerConfig)));

// Add the protectedRoute function to any endpoint routes in the Admin Portal

// Routes
app.use('/api', [routers.healthRouter, routers.developersRouter]);
app.use('/admin', protectedRoute(['Admin']), routers.adminRouter);

// Integrate global error handler after routes to cover all ends.
app.use(middleware.globalErrorHandler);
Expand Down
8 changes: 8 additions & 0 deletions backend/src/routes/admin-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import getBlah from '../controllers/admin-controller';

const router = express.Router();

router.route('/foo').get(getBlah);

export default router;
1 change: 1 addition & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export { default as healthRouter } from './health-router';
export { default as developersRouter } from './developers-route';
export { default as adminRouter } from './admin-router';
34 changes: 34 additions & 0 deletions frontend/src/components/common/DialogModal/DialogModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable react/button-has-type */
// This is copy-pasted from RefreshExpiryDialog in citz-imb-sso-react

import { Dispatch, SetStateAction } from 'react';

type DialogModalProps = {
text: string;
isVisible: boolean;
setIsVisible: Dispatch<SetStateAction<boolean>>;
};

export default function DialogModal({ text, isVisible, setIsVisible }: DialogModalProps) {
if (!isVisible) return null;

return (
<>
<div className="ssor_dialog-overlay" />
<dialog
className="ssor_dialog"
open={isVisible}
>
<div className="ssor_dialog-content">
<p className="ssor_dialog-message">{text}</p>
<button
className="ssor_button"
onClick={() => setIsVisible(false)}
>
Ok
</button>
</div>
</dialog>
</>
);
}
5 changes: 3 additions & 2 deletions frontend/src/components/common/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function Header() {
login({
backendURL: env.VITE_BACKEND_URL,
idpHint: 'idir',
postLoginRedirectURL: '/admin',
});
}
};
Expand All @@ -41,8 +42,8 @@ export default function Header() {
variant="secondary"
size="sm"
disabled={false}
text={isAuthenticated ? 'Logout' : 'Login'}
aria-label={isAuthenticated ? 'Logout' : 'Login'}
text={isAuthenticated ? 'Logout' : 'Admin Login'}
aria-label={isAuthenticated ? 'Logout' : 'Admin Login'}
/>
</ButtonWrapper>
</Banner>
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/routes/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useSSO } from '@bcgov/citz-imb-sso-react';
import { ReactNode } from 'react';
import { Navigate } from 'react-router-dom';

type ProtectedRouteProps = {
requiredRole: string;
children: ReactNode;
};

export default function ProtectedRoute({ requiredRole, children }: ProtectedRouteProps) {
const { hasRoles } = useSSO();

if (!hasRoles([requiredRole])) {
return (
<Navigate
to="/"
replace
state={{ text: 'Sorry, only authorized users can access that page.' }}
/>
);
}
return children;
}
7 changes: 6 additions & 1 deletion frontend/src/routes/ViewRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Routes, Route } from 'react-router-dom';
import LandingPage from '../views/LandingPage/LandingPage';
import FarmInformation from '../views/FarmInformation/FarmInformation';
import AdminDashboard from '@/views/AdminDashboard/AdminDashboard';
import ProtectedRoute from './ProtectedRoute';

export default function ViewRouter() {
return (
Expand All @@ -20,7 +21,11 @@ export default function ViewRouter() {
/>
<Route
path="/admin"
Component={AdminDashboard}
element={
<ProtectedRoute requiredRole="Admin">
<AdminDashboard />
</ProtectedRoute>
}
/>
</Routes>
);
Expand Down
113 changes: 68 additions & 45 deletions frontend/src/views/LandingPage/LandingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
/**
* @summary The landing page for the application
*/
import { useNavigate } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import constants from '../../constants/Constants';
import useAppService from '../../services/app/useAppService';
import { deleteLocalStorageKey } from '../../utils/AppLocalStorage';
import { ButtonWrapper, StyledDivider, StyledContent } from './landingPage.styles';
import { Button, Card } from '../../components/common';
import DialogModal from '@/components/common/DialogModal/DialogModal';

export default function LandingPage() {
const { setNMPFile } = useAppService();
const navigate = useNavigate();

// Props for dialog modal
const location = useLocation();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [modalDialog, _] = useState(
location.state !== null && typeof location.state === 'object' ? location.state.text : undefined,
);
const [isVisible, setIsVisible] = useState<boolean>(modalDialog !== undefined);

useEffect(() => {
// Clear state text to prevent modal appearing on refresh
navigate(location, { replace: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleUpload = () => {
const upload = document.getElementById('fileUp');
if (upload) upload.click();
Expand Down Expand Up @@ -42,49 +58,56 @@ export default function LandingPage() {
};

return (
<Card
width="500px"
height="500px"
justifyContent="center"
>
<StyledContent>
<h1>Nutrient Management Calculator</h1>
<p>
The Nutrient Management Calculator provides a starting point for the efficient use of
fertilizer and manure on farms. This tool assists in you choosing the right rate and
nutrient source for your crops. You can start a new calculation or pick up where you left
off by uploading an existing .nmp file.
</p>
</StyledContent>
<ButtonWrapper>
<Button
text="Start a new calculation"
size="lg"
handleClick={newCalcHandler}
aria-label="Start a new calculation"
variant="primary"
disabled={false}
/>
</ButtonWrapper>
<StyledDivider>or</StyledDivider>
<ButtonWrapper>
<Button
size="lg"
text="Upload an existing .nmp file"
handleClick={handleUpload}
aria-label="Upload an existing .nmp file"
variant="primary"
disabled={false}
/>
<input
id="fileUp"
type="file"
accept=".nmp, application/json"
onChange={saveFile}
aria-label="Upload an existing .nmp file"
hidden
/>
</ButtonWrapper>
</Card>
<>
<Card
width="500px"
height="500px"
justifyContent="center"
>
<StyledContent>
<h1>Nutrient Management Calculator</h1>
<p>
The Nutrient Management Calculator provides a starting point for the efficient use of
fertilizer and manure on farms. This tool assists in you choosing the right rate and
nutrient source for your crops. You can start a new calculation or pick up where you
left off by uploading an existing .nmp file.
</p>
</StyledContent>
<ButtonWrapper>
<Button
text="Start a new calculation"
size="lg"
handleClick={newCalcHandler}
aria-label="Start a new calculation"
variant="primary"
disabled={false}
/>
</ButtonWrapper>
<StyledDivider>or</StyledDivider>
<ButtonWrapper>
<Button
size="lg"
text="Upload an existing .nmp file"
handleClick={handleUpload}
aria-label="Upload an existing .nmp file"
variant="primary"
disabled={false}
/>
<input
id="fileUp"
type="file"
accept=".nmp, application/json"
onChange={saveFile}
aria-label="Upload an existing .nmp file"
hidden
/>
</ButtonWrapper>
</Card>
<DialogModal
text={modalDialog || ''}
isVisible={isVisible}
setIsVisible={setIsVisible}
/>
</>
);
}

0 comments on commit af1b4c7

Please sign in to comment.