Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Clients/src/application/config/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ForgotPassword from "../../presentation/pages/Authentication/ForgotPasswo
import ResetPassword from "../../presentation/pages/Authentication/ResetPassword";
import SetNewPassword from "../../presentation/pages/Authentication/SetNewPassword";
import ResetPasswordContinue from "../../presentation/pages/Authentication/ResetPasswordContinue";
import MicrosoftCallback from "../../presentation/pages/Authentication/MicrosoftCallback";
import ProjectView from "../../presentation/pages/ProjectView";
import FileManager from "../../presentation/pages/FileManager";
import Reporting from "../../presentation/pages/Reporting";
Expand Down Expand Up @@ -112,6 +113,11 @@ export const createRoutes = (
path="/reset-password-continue"
element={<ProtectedRoute Component={ResetPasswordContinue} />}
/>,
<Route
key="microsoft-callback"
path="/auth/microsoft/callback"
element={<MicrosoftCallback />}
/>,
<Route key="playground" path="/playground" element={<Playground />} />,
// <Route key="public" path="/public" element={<AITrustCentrePublic />} />,
<Route key="aiTrustCentrepublic" path="/aiTrustCentre/:hash" element={<AITrustCentrePublic />} />,
Expand Down
4 changes: 4 additions & 0 deletions Clients/src/application/constants/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ const allowedRoles = {
view: ["Admin"],
manage: ["Admin"],
},
sso: {
view: ["Admin"],
manage: ["Admin"],
},
};

export default allowedRoles;
63 changes: 63 additions & 0 deletions Clients/src/application/repository/ssoConfig.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { GetRequestParams, RequestParams } from "../../domain/interfaces/iRequestParams";
import { apiServices } from "../../infrastructure/api/networkServices";

/**
* Retrieves SSO configuration for an organization.
*
* @param {GetRequestParams} params - The parameters for the request.
* @returns {Promise<any>} The SSO configuration data retrieved from the API.
* @throws Will throw an error if the request fails.
*/
export async function GetSsoConfig({
routeUrl,
signal,
responseType = "json",
}: GetRequestParams): Promise<any> {
try {
const response = await apiServices.get(routeUrl, {
signal,
responseType,
});
return response;
} catch (error) {
throw error;
}
}

/**
* Updates SSO configuration for an organization.
*
* @param {RequestParams} params - The parameters for updating the SSO configuration.
* @returns {Promise<any>} A promise that resolves to the updated SSO configuration data.
* @throws Will throw an error if the update operation fails.
*/
export async function UpdateSsoConfig({
routeUrl,
body,
}: RequestParams): Promise<any> {
try {
const response = await apiServices.put(routeUrl, body);
return response.data;
} catch (error) {
throw error;
}
}

/**
* Enables or disables SSO for an organization.
*
* @param {RequestParams} params - The parameters for enabling/disabling SSO.
* @returns {Promise<any>} A promise that resolves to the response data.
* @throws Will throw an error if the operation fails.
*/
export async function ToggleSsoStatus({
routeUrl,
body,
}: RequestParams): Promise<any> {
try {
const response = await apiServices.put(routeUrl, body);
return response.data;
} catch (error) {
throw error;
}
}
38 changes: 25 additions & 13 deletions Clients/src/application/repository/user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,37 @@ export async function deleteUserById({
export async function checkUserExists(): Promise<any> {
try {
const response = await apiServices.get(`/users/check/exists`);
return response.data;
return response.data;
} catch (error) {
console.error("Error checking if user exists:", error);
throw error;
}
}

export async function loginUser({
body,
}: {
body: any;
}): Promise<any> {
try {
const response = await apiServices.post(`/users/login`, body);
return response;
} catch (error) {
console.error("Error logging in user:", error);
throw error;
}
body,
}: {
body: any;
}): Promise<any> {
try {
const response = await apiServices.post(`/users/login`, body);
return response;
} catch (error) {
console.error("Error logging in user:", error);
throw error;
}
}


export async function loginUserWithMicrosoft({
code,
}: {
code: string;
}): Promise<any> {
try {
const response = await apiServices.post(`/users/login-microsoft`, { code });
return response;
} catch (error) {
console.error("Error logging in with Microsoft:", error);
throw error;
}
}
2 changes: 2 additions & 0 deletions Clients/src/domain/types/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type User = {
organization_id?: number; //organization association
pwd_set?: boolean; //password set flag (compatibility)
data?: any; //compatibility property for API responses
sso_provider?: string | null;
sso_user_id?: string | null;
}

export interface ApiResponse<T> {
Expand Down
6 changes: 6 additions & 0 deletions Clients/src/presentation/assets/icons/microsoft-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 137 additions & 0 deletions Clients/src/presentation/components/MicrosoftSignIn/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Button, useTheme } from "@mui/material"
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setAuthToken, setExpiration } from "../../../application/redux/auth/authSlice";
import { ReactComponent as MicrosoftIcon } from "../../assets/icons/microsoft-icon.svg";

interface MicrosoftSignInProps {
isSubmitting: boolean;
setIsSubmitting: (isSubmitting: boolean) => void;
tenantId?: string;
clientId?: string;
text?: string;
}

export const MicrosoftSignIn: React.FC<MicrosoftSignInProps> = ({
isSubmitting,
setIsSubmitting,
tenantId,
clientId,
text = "Sign in with Microsoft"
}) => {
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();

const [_, setAlert] = useState<{
variant: "success" | "info" | "warning" | "error";
title?: string;
body: string;
} | null>(null);

// Listen for messages from the popup window
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Verify the message is from our origin
if (event.origin !== window.location.origin) return;

if (event.data.type === 'MICROSOFT_AUTH_SUCCESS') {
dispatch(setAuthToken(event.data.token));
dispatch(setExpiration(event.data.expirationDate));
localStorage.setItem('root_version', __APP_VERSION__);
setIsSubmitting(false);
navigate("/");
} else if (event.data.type === 'MICROSOFT_AUTH_ERROR') {
setIsSubmitting(false);
setAlert({
variant: "error",
body: event.data.error || "SSO authentication failed",
});
setTimeout(() => setAlert(null), 3000);
}
};

window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [dispatch, navigate, setIsSubmitting]);

// Handle Microsoft Sign-in
const handleMicrosoftSignIn = async () => {
if (!tenantId || !clientId) {
setAlert({
variant: "error",
body: "Microsoft Sign-In is not configured. Please contact your administrator.",
});
setTimeout(() => setAlert(null), 3000);
return;
}

try {
setIsSubmitting(true);

// Construct Microsoft OAuth URL
const redirectUri = `${window.location.origin}/auth/microsoft/callback`;
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` +
`client_id=${clientId}&` +
`response_type=code&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=openid profile email&` +
`response_mode=query`;

// Open Microsoft login in new tab
window.open(authUrl, '_blank');
setIsSubmitting(false);
} catch (error: any) {
setIsSubmitting(false);
setAlert({
variant: "error",
body: "Failed to initiate Microsoft Sign-In. Please try again.",
});
setTimeout(() => setAlert(null), 3000);
}
};

return (
<Button
type="button"
disableRipple
variant="contained"
sx={{
height: 34,
fontSize: '13px',
backgroundColor: '#2f2f2f',
color: '#ffffff',
boxShadow: 'none',
textTransform: 'none',
borderRadius: '4px',
border: 'none',
position: 'relative',
paddingLeft: theme.spacing(6),
'&:hover': {
backgroundColor: '#1a1a1a',
boxShadow: 'none',
},
'&:disabled': {
backgroundColor: '#cccccc',
color: '#666666',
},
}}
onClick={handleMicrosoftSignIn}
disabled={isSubmitting || !tenantId || !clientId}
>
<MicrosoftIcon
style={{
position: "absolute",
left: theme.spacing(2.5),
width: "30px",
height: "27px",
backgroundColor: "white",
borderRadius: "4px",
padding: "2px",
}}
/>
{!tenantId || !clientId ? "Microsoft Sign-In Not Configured" : text}
</Button>
)
}
65 changes: 63 additions & 2 deletions Clients/src/presentation/pages/Authentication/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, Stack, Typography, useTheme } from "@mui/material";
import React, { Suspense, useState } from "react";
import { Button, Divider, Stack, Typography, useTheme } from "@mui/material";
import React, { Suspense, useState, useEffect } from "react";
import { ReactComponent as Background } from "../../../assets/imgs/background-grid.svg";
import Checkbox from "../../../components/Inputs/Checkbox";
import Field from "../../../components/Inputs/Field";
Expand All @@ -14,6 +14,8 @@ import Alert from "../../../components/Alert";
import { ENV_VARs } from "../../../../../env.vars";
import { useIsMultiTenant } from "../../../../application/hooks/useIsMultiTenant";
import { loginUser } from "../../../../application/repository/user.repository";
import { MicrosoftSignIn } from "../../../components/MicrosoftSignIn";
import { GetSsoConfig } from "../../../../application/repository/ssoConfig.repository";

const isDemoApp = ENV_VARs.IS_DEMO_APP || false;

Expand Down Expand Up @@ -50,6 +52,36 @@ const Login: React.FC = () => {
body: string;
} | null>(null);

// SSO configuration state
const [ssoConfig, setSsoConfig] = useState<{
tenantId?: string;
clientId?: string;
isEnabled?: boolean;
}>({});

// Fetch SSO configuration on mount
useEffect(() => {
const fetchSsoConfig = async () => {
try {
const response = await GetSsoConfig({
routeUrl: `ssoConfig?provider=AzureAD`,
});

if (response?.data && response.data.is_enabled) {
setSsoConfig({
tenantId: response.data.config_data?.tenant_id,
clientId: response.data.config_data?.client_id,
isEnabled: response.data.is_enabled,
});
}
} catch (error) {
console.error('Failed to fetch SSO config:', error);
}
};

fetchSsoConfig();
}, []);

// Handle changes in input fields
const handleChange =
(prop: keyof FormValues) =>
Expand Down Expand Up @@ -215,6 +247,35 @@ const Login: React.FC = () => {
{loginText}
</Typography>
<Stack sx={{ gap: theme.spacing(7.5) }}>
{ssoConfig.isEnabled && ssoConfig.tenantId && ssoConfig.clientId && (
<>
<MicrosoftSignIn
isSubmitting={isSubmitting}
setIsSubmitting={setIsSubmitting}
tenantId={ssoConfig.tenantId}
clientId={ssoConfig.clientId}
text="Sign in with Microsoft"
/>
<Stack sx={{ position: 'relative', my: 2 }}>
<Divider />
<Typography
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
px: 2,
fontSize: 14,
color: theme.palette.text.secondary,
fontWeight: 500,
}}
>
or
</Typography>
</Stack>
</>
)}
<Field
label="Email"
isRequired
Expand Down
Loading