Skip to content

Commit

Permalink
feat: add /auth page
Browse files Browse the repository at this point in the history
  • Loading branch information
aldbr committed Nov 22, 2023
1 parent 91bb1e7 commit 8dc40ed
Show file tree
Hide file tree
Showing 16 changed files with 424 additions and 72 deletions.
Binary file added public/DIRAC-logo-minimal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from "react";

export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
5 changes: 5 additions & 0 deletions src/app/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LoginForm } from "@/components/ui/LoginForm";

export default function Page() {
return <LoginForm />;
}
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Showcase from "@/components/layout/Showcase";
import Showcase from "@/components/applications/Showcase";

export default function Page() {
return <Showcase />;
Expand Down
File renamed without changes.
4 changes: 3 additions & 1 deletion src/components/applications/UserDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export default function UserDashboard() {
mr: "5%",
}}
>
<span>Hello {accessTokenPayload["preferred_username"]}</span>
<h2>Hello {accessTokenPayload["preferred_username"]}</h2>

<p>To start with, select an application in the side bar</p>
</Box>
</MUIThemeProvider>
</React.Fragment>
Expand Down
27 changes: 14 additions & 13 deletions src/components/auth/OIDCUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"use client";
import {
OidcConfiguration,
OidcProvider,
OidcSecure,
} from "@axa-fr/react-oidc";
import { OidcConfiguration, OidcProvider, useOidc } from "@axa-fr/react-oidc";
import React, { useState, useEffect } from "react";
import { useDiracxUrl } from "@/hooks/utils";
import { useRouter } from "next/navigation";

interface OIDCProviderProps {
children: React.ReactNode;
Expand Down Expand Up @@ -65,15 +62,19 @@ export function OIDCProvider(props: OIDCProviderProps) {
}

/**
* Wrapper around the react-oidc OidcSecure component
* Needed because OidcProvider cannot be directly called from a server file
* Check whether the user is authenticated, and redirect to the login page if not
* @param props - configuration of the OIDC provider
* @returns the wrapper around OidcProvider
* @returns The children if the user is authenticated, null otherwise
*/
export function OIDCSecure({ children }: OIDCProps) {
return (
<>
<OidcSecure>{children}</OidcSecure>
</>
);
const { isAuthenticated } = useOidc();
const router = useRouter();

// Redirect to login page if not authenticated
if (!isAuthenticated) {
router.push("/auth");
return null;
}

return <>{children}</>;
}
2 changes: 1 addition & 1 deletion src/components/ui/DiracLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function DiracLogo() {
return (
<>
<NextLink href="/">
<Image src="/DIRAC-logo.png" alt="DIRAC logo" width={150} height={55} />
<Image src="/DIRAC-logo.png" alt="DIRAC logo" width={150} height={45} />
</NextLink>
</>
);
Expand Down
55 changes: 30 additions & 25 deletions src/components/ui/LoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Button,
Divider,
IconButton,
Link,
ListItemIcon,
Menu,
MenuItem,
Expand All @@ -20,10 +21,11 @@ import React from "react";
*/
export function LoginButton() {
const { accessTokenPayload } = useOidcAccessToken();
const { login, logout, isAuthenticated } = useOidc();
const { logout, isAuthenticated } = useOidc();

const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);

const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
Expand All @@ -42,7 +44,8 @@ export function LoginButton() {
"&:hover": { bgcolor: deepOrange[500] },
}}
variant="contained"
onClick={() => login()}
component={Link}
href="/auth"
>
Login
</Button>
Expand All @@ -69,29 +72,31 @@ export function LoginButton() {
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 0,
sx: {
overflow: "visible",
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
mt: 1.5,
"& .MuiAvatar-root": {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
"&:before": {
content: '""',
display: "block",
position: "absolute",
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: "background.paper",
transform: "translateY(-50%) rotate(45deg)",
zIndex: 0,
slotProps={{
paper: {
elevation: 0,
sx: {
overflow: "visible",
filter: "drop-shadow(0px 2px 8px rgba(0,0,0,0.32))",
mt: 1.5,
"& .MuiAvatar-root": {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
"&:before": {
content: '""',
display: "block",
position: "absolute",
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: "background.paper",
transform: "translateY(-50%) rotate(45deg)",
zIndex: 0,
},
},
},
}}
Expand Down
201 changes: 201 additions & 0 deletions src/components/ui/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"use client";

import { useState, useEffect } from "react";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Button from "@mui/material/Button";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import { useMetadata } from "@/hooks/metadata";
import { deepOrange, lightGreen } from "@mui/material/colors";
import NextLink from "next/link";
import Image from "next/image";
import { CssBaseline, Stack } from "@mui/material";
import { OidcConfiguration, useOidc } from "@axa-fr/react-oidc";
import { useMUITheme } from "@/hooks/theme";
import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles";
import { useRouter } from "next/navigation";
import { useDiracxUrl } from "@/hooks/utils";

/**
* Login form
* @returns a form
*/
export function LoginForm() {
const { login, isAuthenticated } = useOidc();
const theme = useMUITheme();
const router = useRouter();
const { data, error, isLoading } = useMetadata();
const [selectedVO, setSelectedVO] = useState<string | null>(null);
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
const [configuration, setConfiguration] = useState<OidcConfiguration | null>(
null,
);
const diracxUrl = useDiracxUrl();

// Set OIDC configuration
useEffect(() => {
if (diracxUrl !== null && selectedVO !== null && selectedGroup !== null) {
setConfiguration(() => ({
authority: diracxUrl,
// TODO: Figure out how to get this. Hardcode? Get from a /.well-known/diracx-configuration endpoint?
client_id: "myDIRACClientID",
scope: `vo:${selectedVO} group:${selectedGroup}`,
redirect_uri: `${diracxUrl}/#authentication-callback`,
}));
}
}, [diracxUrl]);

// Set default VO if only one is available
useEffect(() => {
if (data) {
const vos = Object.keys(data.virtual_organizations);
if (vos.length === 1) {
setSelectedVO(vos[0]);
}
}
}, [data]);

// Redirect to dashboard if already authenticated
if (isAuthenticated) {
router.push("/dashboard");
return null;
}

// Set group
const handleGroupChange = (event: SelectChangeEvent) => {
const value = event.target.value;
setSelectedGroup(value);
};

// Login
const handleLogin = () => {
login();
};

if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div>An error occurred while fetching metadata.</div>;
}
if (!data) {
return <div>No metadata found.</div>;
}

// Is there only one VO?
const singleVO = data && Object.keys(data.virtual_organizations).length === 1;

return (
<MUIThemeProvider theme={theme}>
<CssBaseline />

<Box
sx={{
ml: { xs: "5%", md: "30%" },
mr: { xs: "5%", md: "30%" },
}}
>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
paddingTop: "10%",
paddingBottom: "10%",
}}
>
<NextLink href="/">
<Image
src="/DIRAC-logo-minimal.png"
alt="DIRAC logo"
width={150}
height={150}
/>
</NextLink>
</Box>
{singleVO ? (
<Typography variant="h3" gutterBottom sx={{ textAlign: "center" }}>
{selectedVO}
</Typography>
) : (
<Autocomplete
options={Object.keys(data.virtual_organizations)}
getOptionLabel={(option) => option}
renderInput={(params) => (
<TextField
{...params}
label="Select Virtual Organization"
variant="outlined"
/>
)}
value={selectedVO}
onChange={(event: any, newValue: string | null) => {
setSelectedVO(newValue);
}}
sx={{
"& .MuiAutocomplete-root": {
// Style changes when an option is selected
opacity: selectedVO ? 0.5 : 1,
},
}}
/>
)}
{selectedVO && (
<Box sx={{ mt: 4 }}>
<FormControl fullWidth>
<InputLabel>Select a Group</InputLabel>
<Select
name={selectedVO}
value={
selectedGroup ||
data.virtual_organizations[selectedVO].default_group
}
label="Select a Group"
onChange={handleGroupChange}
>
{Object.keys(data.virtual_organizations[selectedVO].groups).map(
(groupKey) => (
<MenuItem key={groupKey} value={groupKey}>
{groupKey}
</MenuItem>
),
)}
</Select>
</FormControl>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
sx={{ mt: 5, width: "100%" }}
>
<Button
variant="contained"
sx={{
bgcolor: lightGreen[700],
"&:hover": { bgcolor: deepOrange[500] },
flexGrow: 1,
}}
onClick={handleLogin}
>
Login through your Identity Provider
</Button>
<Button variant="outlined" onClick={handleLogin}>
Advanced Options
</Button>
</Stack>
<Typography
sx={{ paddingTop: "5%", color: "gray", textAlign: "center" }}
>
Need help?{" "}
{data.virtual_organizations[selectedVO].support.message}
</Typography>
</Box>
)}
</Box>
</MUIThemeProvider>
);
}
Loading

0 comments on commit 8dc40ed

Please sign in to comment.