diff --git a/Clients/src/application/config/routes.tsx b/Clients/src/application/config/routes.tsx
index 499d4a196e..650c5bdd5d 100644
--- a/Clients/src/application/config/routes.tsx
+++ b/Clients/src/application/config/routes.tsx
@@ -14,6 +14,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 FileManager from "../../presentation/pages/FileManager";
import Reporting from "../../presentation/pages/Reporting";
import VWHome from "../../presentation/pages/Home/1.0Home";
@@ -144,7 +145,12 @@ export const createRoutes = (
path="/reset-password-continue"
element={}
/>,
-// } />,
+ }
+ />,
+ // } />,
} />,
} />,
// Style Guide - Development only
diff --git a/Clients/src/application/constants/permissions.ts b/Clients/src/application/constants/permissions.ts
index 5722356c1c..bcb48bd9bd 100644
--- a/Clients/src/application/constants/permissions.ts
+++ b/Clients/src/application/constants/permissions.ts
@@ -67,6 +67,10 @@ const allowedRoles = {
deleteDataset: ["Admin", "Editor"],
manageApiKeys: ["Admin"],
},
+ sso: {
+ view: ["Admin"],
+ manage: ["Admin"],
+ },
};
export default allowedRoles;
diff --git a/Clients/src/application/repository/organization.repository.ts b/Clients/src/application/repository/organization.repository.ts
index 9f48df25ad..174a9e5c57 100644
--- a/Clients/src/application/repository/organization.repository.ts
+++ b/Clients/src/application/repository/organization.repository.ts
@@ -81,3 +81,19 @@ export async function checkOrganizationExists(): Promise {
throw error;
}
}
+
+/**
+ * Retrieves all organizations in the system (public endpoint for login).
+ *
+ * @returns {Promise} List of all organizations.
+ * @throws Will throw an error if the request fails.
+ */
+export async function getAllOrganizations(): Promise {
+ try {
+ const response = await apiServices.get("/organizations");
+ const data = response.data as { data?: any[] };
+ return data?.data ?? [];
+ } catch (error) {
+ throw error;
+ }
+}
diff --git a/Clients/src/application/repository/ssoConfig.repository.ts b/Clients/src/application/repository/ssoConfig.repository.ts
new file mode 100644
index 0000000000..cc006eaae7
--- /dev/null
+++ b/Clients/src/application/repository/ssoConfig.repository.ts
@@ -0,0 +1,88 @@
+import { GetRequestParams, RequestParams } from "../../domain/interfaces/i.requestParams";
+import { apiServices } from "../../infrastructure/api/networkServices";
+
+/**
+ * Retrieves SSO configuration for an organization.
+ *
+ * @param {GetRequestParams} params - The parameters for the request.
+ * @returns {Promise} 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 {
+ 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} 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 {
+ 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} 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 {
+ try {
+ const response = await apiServices.put(routeUrl, body);
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
+
+/**
+ * Check SSO status for a specific organization (public endpoint for login).
+ *
+ * @param {number} organizationId - The ID of the organization.
+ * @param {string} provider - The SSO provider (e.g., 'AzureAD').
+ * @returns {Promise} A promise that resolves to SSO status data.
+ * @throws Will throw an error if the operation fails.
+ */
+export async function CheckSsoStatusByOrgId({
+ organizationId,
+ provider = 'AzureAD',
+}: {
+ organizationId: number;
+ provider?: string;
+}): Promise {
+ try {
+ const response = await apiServices.get(
+ `ssoConfig/check-status?organizationId=${organizationId}&provider=${provider}`
+ );
+ return response.data;
+ } catch (error) {
+ throw error;
+ }
+}
diff --git a/Clients/src/application/repository/user.repository.ts b/Clients/src/application/repository/user.repository.ts
index 15fe4c0be8..cdbe6e3274 100644
--- a/Clients/src/application/repository/user.repository.ts
+++ b/Clients/src/application/repository/user.repository.ts
@@ -215,3 +215,19 @@ export async function deleteUserProfilePhoto(userId: number | string): Promise(`/users/${userId}/profile-photo`);
return response;
}
+
+export async function loginUserWithMicrosoft({
+ code,
+ organizationId,
+}: {
+ code: string;
+ organizationId: number;
+}): Promise {
+ try {
+ const response = await apiServices.post(`/users/login-microsoft`, { code, organizationId });
+ return response;
+ } catch (error) {
+ console.error("Error logging in with Microsoft:", error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/Clients/src/domain/types/User.ts b/Clients/src/domain/types/User.ts
index bd2ac2565a..11920f0d6d 100644
--- a/Clients/src/domain/types/User.ts
+++ b/Clients/src/domain/types/User.ts
@@ -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 {
diff --git a/Clients/src/presentation/assets/icons/microsoft-icon.svg b/Clients/src/presentation/assets/icons/microsoft-icon.svg
new file mode 100644
index 0000000000..8a7cb18b85
--- /dev/null
+++ b/Clients/src/presentation/assets/icons/microsoft-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/Clients/src/presentation/components/Inputs/SelectField/index.tsx b/Clients/src/presentation/components/Inputs/SelectField/index.tsx
new file mode 100644
index 0000000000..cd71324607
--- /dev/null
+++ b/Clients/src/presentation/components/Inputs/SelectField/index.tsx
@@ -0,0 +1,168 @@
+/**
+ * A customizable select field component that matches the Field component's styling.
+ * Provides consistent styling with text inputs for organization selection.
+ */
+
+import {
+ Select,
+ MenuItem,
+ Stack,
+ Typography,
+ useTheme,
+ SelectChangeEvent,
+} from "@mui/material";
+import { getInputStyles } from "../../../utils/inputStyles";
+
+interface SelectFieldProps {
+ id?: string;
+ label?: string;
+ isRequired?: boolean;
+ isOptional?: boolean;
+ optionalLabel?: string;
+ placeholder?: string;
+ value: string | number;
+ onChange: (event: SelectChangeEvent) => void;
+ options: Array<{ id: number | string; name: string }>;
+ error?: string;
+ disabled?: boolean;
+ width?: string | number;
+ sx?: any;
+ loading?: boolean;
+}
+
+const SelectField = ({
+ id,
+ label,
+ isRequired,
+ isOptional,
+ optionalLabel,
+ placeholder,
+ value,
+ onChange,
+ options,
+ error,
+ disabled,
+ width,
+ sx,
+ loading = false,
+}: SelectFieldProps) => {
+ const theme = useTheme();
+
+ const rootSx = sx;
+
+ return (
+
+ {label && (
+
+ {label}
+ {isRequired ? (
+
+ *
+
+ ) : (
+ ""
+ )}
+ {isOptional ? (
+
+ {optionalLabel || "(optional)"}
+
+ ) : (
+ ""
+ )}
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default SelectField;
diff --git a/Clients/src/presentation/components/MicrosoftSignIn/index.tsx b/Clients/src/presentation/components/MicrosoftSignIn/index.tsx
new file mode 100644
index 0000000000..0e1a73ecb7
--- /dev/null
+++ b/Clients/src/presentation/components/MicrosoftSignIn/index.tsx
@@ -0,0 +1,139 @@
+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;
+ organizationId?: number;
+ text?: string;
+}
+
+export const MicrosoftSignIn: React.FC = ({
+ isSubmitting,
+ setIsSubmitting,
+ tenantId,
+ clientId,
+ organizationId,
+ 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 (
+
+ )
+}
diff --git a/Clients/src/presentation/pages/Authentication/Login/index.tsx b/Clients/src/presentation/pages/Authentication/Login/index.tsx
index 00e2c66828..b913684b26 100644
--- a/Clients/src/presentation/pages/Authentication/Login/index.tsx
+++ b/Clients/src/presentation/pages/Authentication/Login/index.tsx
@@ -1,8 +1,9 @@
-import { Button, Stack, Typography, useTheme, Box } from "@mui/material";
-import React, { Suspense, useState } from "react";
+import { Button, Stack, Typography, useTheme, Box, Divider } from "@mui/material";
+import React, { Suspense, useEffect, useState } from "react";
import { ReactComponent as Background } from "../../../assets/imgs/background-grid.svg";
import Checkbox from "../../../components/Inputs/Checkbox";
import Field from "../../../components/Inputs/Field";
+import SelectField from "../../../components/Inputs/SelectField";
import singleTheme from "../../../themes/v1SingleTheme";
import { useNavigate } from "react-router-dom";
import { logEngine } from "../../../../application/tools/log.engine";
@@ -13,6 +14,9 @@ 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 { CheckSsoStatusByOrgId } from "../../../../application/repository/ssoConfig.repository";
+import { getAllOrganizations } from "../../../../application/repository/organization.repository";
// Animated loading component specifically for login
const LoginLoadingOverlay: React.FC = () => {
@@ -130,6 +134,76 @@ const Login: React.FC = () => {
body: string;
} | null>(null);
+ // Organization selection state
+ const [organizations, setOrganizations] = useState([]);
+ const [selectedOrgId, setSelectedOrgId] = useState(null);
+ const [loadingOrgs, setLoadingOrgs] = useState(true);
+
+ // SSO configuration state
+ const [ssoConfig, setSsoConfig] = useState<{
+ tenantId?: string;
+ clientId?: string;
+ isEnabled?: boolean;
+ }>({});
+
+ // Fetch all organizations on mount
+ useEffect(() => {
+ const fetchOrganizations = async () => {
+ try {
+ setLoadingOrgs(true);
+ const orgs = await getAllOrganizations();
+ setOrganizations(orgs);
+ } catch (error) {
+ console.error('Failed to fetch organizations:', error);
+ setOrganizations([]);
+ setAlert({
+ variant: "error",
+ body: "Failed to load organizations. Please refresh the page."
+ });
+ } finally {
+ setLoadingOrgs(false);
+ }
+ };
+
+ fetchOrganizations();
+ }, []);
+
+ // Fetch SSO configuration when organization is selected
+ useEffect(() => {
+ const fetchSsoConfig = async () => {
+ if (!selectedOrgId) {
+ setSsoConfig({});
+ return;
+ }
+
+ try {
+ const response = await CheckSsoStatusByOrgId({
+ organizationId: selectedOrgId,
+ provider: 'AzureAD',
+ });
+
+ if (response?.isEnabled && response?.hasConfig) {
+ setSsoConfig({
+ tenantId: response.tenantId,
+ clientId: response.clientId,
+ isEnabled: response.isEnabled,
+ });
+ // Store organization ID for Microsoft callback
+ sessionStorage.setItem('sso_organization_id', selectedOrgId.toString());
+ } else {
+ setSsoConfig({});
+ sessionStorage.removeItem('sso_organization_id');
+ }
+ } catch (error) {
+ console.error('Failed to fetch SSO config:', error);
+ setSsoConfig({});
+ sessionStorage.removeItem('sso_organization_id');
+ }
+ };
+
+ fetchSsoConfig();
+ }, [selectedOrgId]);
+
// Handle changes in input fields
const handleChange =
(prop: keyof FormValues) =>
@@ -140,6 +214,13 @@ const Login: React.FC = () => {
// Handle form submission
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
+
+ if (!selectedOrgId && organizations.length > 0) {
+ setAlert({ variant: "error", body: "Please select an organization" });
+ setTimeout(() => setAlert(null), 5000);
+ return;
+ }
+
setIsSubmitting(true);
await loginUser({
@@ -286,7 +367,7 @@ const Login: React.FC = () => {
{loginText}
-
+
{
)}
+
+ {/* Divider with "or" */}
+
+
+
+ or
+
+
+
+ {/* Organization Selection - Always show at bottom */}
+ setSelectedOrgId(Number(e.target.value))}
+ options={organizations.map(org => ({ id: org.id, name: org.name }))}
+ disabled={loadingOrgs || organizations.length === 0}
+ loading={loadingOrgs}
+ />
+
+ {/* Microsoft SSO Button - Always show when organization is selected */}
+ {selectedOrgId && (
+
+ )}
diff --git a/Clients/src/presentation/pages/Authentication/MicrosoftCallback/index.tsx b/Clients/src/presentation/pages/Authentication/MicrosoftCallback/index.tsx
new file mode 100644
index 0000000000..d0b9aeef0c
--- /dev/null
+++ b/Clients/src/presentation/pages/Authentication/MicrosoftCallback/index.tsx
@@ -0,0 +1,123 @@
+import React, { useEffect, useState } from "react";
+import { Box, CircularProgress, Typography } from "@mui/material";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { setAuthToken, setExpiration } from "../../../../application/redux/auth/authSlice";
+import { loginUserWithMicrosoft } from "../../../../application/repository/user.repository";
+
+const MicrosoftCallback: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ const handleCallback = async () => {
+ const code = searchParams.get("code");
+ const errorParam = searchParams.get("error");
+ const errorDescription = searchParams.get("error_description");
+
+ // Handle error from Microsoft
+ if (errorParam) {
+ setError(errorDescription || "Authentication failed");
+ setTimeout(() => {
+ navigate("/login", { state: { error: errorDescription || "SSO authentication failed" } });
+ }, 3000);
+ return;
+ }
+
+ // Handle missing code
+ if (!code) {
+ setError("No authorization code received");
+ setTimeout(() => {
+ navigate("/login", { state: { error: "Invalid SSO callback" } });
+ }, 3000);
+ return;
+ }
+
+ try {
+ // Retrieve organization ID from session storage (set during organization selection on login page)
+ const orgIdFromSession = sessionStorage.getItem('sso_organization_id');
+ const organizationId = orgIdFromSession ? parseInt(orgIdFromSession, 10) : 1; // Default to 1 if not found
+
+ const response = await loginUserWithMicrosoft({ code, organizationId });
+
+ if (response.status === 202 || response.status === 200) {
+ const token = response.data.data.token;
+
+ // Always remember Microsoft sign-in for 30 days
+ const expirationDate = Date.now() + 30 * 24 * 60 * 60 * 1000;
+
+ // If opened in popup, send message to parent and close
+ if (window.opener) {
+ window.opener.postMessage(
+ {
+ type: 'MICROSOFT_AUTH_SUCCESS',
+ token,
+ expirationDate
+ },
+ window.location.origin
+ );
+ window.close();
+ } else {
+ // If not a popup, use regular flow
+ dispatch(setAuthToken(token));
+ dispatch(setExpiration(expirationDate));
+ localStorage.setItem('root_version', __APP_VERSION__);
+ navigate("/");
+ }
+ } else {
+ throw new Error("Authentication failed");
+ }
+ } catch (err) {
+ setError("Authentication failed. Please try again.");
+ // If opened in popup, notify parent of error
+ if (window.opener) {
+ window.opener.postMessage(
+ {
+ type: 'MICROSOFT_AUTH_ERROR',
+ error: 'SSO authentication failed'
+ },
+ window.location.origin
+ );
+ setTimeout(() => window.close(), 3000);
+ } else {
+ setTimeout(() => {
+ navigate("/login", { state: { error: "SSO authentication failed" } });
+ }, 3000);
+ }
+ }
+ };
+
+ handleCallback();
+ }, [searchParams, navigate]);
+
+ return (
+
+ {error ? (
+ <>
+
+ {error}
+
+
+ Redirecting to login...
+
+ >
+ ) : (
+ <>
+
+ Completing sign in...
+ >
+ )}
+
+ );
+};
+
+export default MicrosoftCallback;
diff --git a/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/SsoConfigTab.tsx b/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/SsoConfigTab.tsx
new file mode 100644
index 0000000000..13608a6bac
--- /dev/null
+++ b/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/SsoConfigTab.tsx
@@ -0,0 +1,407 @@
+import React, { useState, useCallback, useEffect } from "react";
+import {
+ Box,
+ Stack,
+ Typography,
+ useTheme,
+ CircularProgress,
+} from "@mui/material";
+import Field from "../../../components/Inputs/Field";
+import Alert from "../../../components/Alert";
+import Button from "../../../components/Button";
+import Select from "../../../components/Inputs/Select";
+import { useAuth } from "../../../../application/hooks/useAuth";
+import { GetSsoConfig, UpdateSsoConfig, ToggleSsoStatus } from "../../../../application/repository/ssoConfig.repository";
+
+// State interface for SSO Configuration (MVP)
+interface SsoConfig {
+ tenantId: string;
+ clientId: string;
+ clientSecret: string;
+ cloudEnvironment: string;
+ isEnabled: boolean;
+ authMethodPolicy: 'sso_only' | 'password_only' | 'both';
+}
+
+// Validation errors interface
+interface ValidationErrors {
+ tenantId?: string;
+ clientId?: string;
+ clientSecret?: string;
+}
+
+// Cloud environment options
+const cloudEnvironments = [
+ { _id: "AzurePublic", name: "Azure Public Cloud" },
+ { _id: "AzureGovernment", name: "Azure Government" }
+];
+
+// Authentication method policy options
+// const authMethodPolicies = [
+// { _id: "both", name: "Allow both SSO and password authentication" },
+// { _id: "sso_only", name: "Require SSO authentication only" },
+// { _id: "password_only", name: "Allow password authentication only" }
+// ];
+
+const SsoConfigTab: React.FC = () => {
+ const { organizationId } = useAuth();
+ const [config, setConfig] = useState({
+ tenantId: "",
+ clientId: "",
+ clientSecret: "",
+ cloudEnvironment: "AzurePublic",
+ isEnabled: false,
+ authMethodPolicy: "both",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [isSaving, setIsSaving] = useState(false);
+ const [isEnabling, setIsEnabling] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const theme = useTheme();
+
+ // Fetch SSO config on mount
+ useEffect(() => {
+ const fetchSsoConfig = async () => {
+ if (!organizationId) {
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ const response = await GetSsoConfig({
+ routeUrl: `ssoConfig?organizationId=${organizationId}&provider=AzureAD`,
+ });
+
+ if (response?.data) {
+ // Map backend response to frontend state
+ setConfig({
+ tenantId: response.data.config_data?.tenant_id || "",
+ clientId: response.data.config_data?.client_id || "",
+ clientSecret: response.data.config_data?.client_secret || "",
+ cloudEnvironment: response.data.config_data?.cloud_environment || "AzurePublic",
+ isEnabled: response.data.is_enabled || false,
+ authMethodPolicy: "both",
+ });
+ }
+ // If error or no config found, keep default empty state
+ } catch (error) {
+ console.error('Failed to fetch SSO config:', error);
+ // Keep default empty state on error
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchSsoConfig();
+ }, [organizationId]);
+
+ // Validation functions
+ const validateUUID = (value: string): boolean => {
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+ return uuidRegex.test(value);
+ };
+
+ const validateField = useCallback((field: keyof ValidationErrors, value: string) => {
+ const newErrors = { ...errors };
+
+ switch (field) {
+ case 'tenantId':
+ if (!value) {
+ newErrors.tenantId = "Tenant ID is required";
+ } else if (!validateUUID(value)) {
+ newErrors.tenantId = "Please enter a valid UUID format";
+ } else {
+ delete newErrors.tenantId;
+ }
+ break;
+ case 'clientId':
+ if (!value) {
+ newErrors.clientId = "Client ID is required";
+ } else if (!validateUUID(value)) {
+ newErrors.clientId = "Please enter a valid UUID format";
+ } else {
+ delete newErrors.clientId;
+ }
+ break;
+ case 'clientSecret':
+ if (!value) {
+ newErrors.clientSecret = "Client Secret is required";
+ } else if (value.length < 10) {
+ newErrors.clientSecret = "Client Secret must be at least 10 characters";
+ } else {
+ delete newErrors.clientSecret;
+ }
+ break;
+ }
+
+ setErrors(newErrors);
+ }, [errors]);
+
+ const handleFieldChange = (field: keyof SsoConfig) => (
+ event: React.ChangeEvent
+ ) => {
+ const value = event.target.value;
+ setConfig(prev => ({ ...prev, [field]: value }));
+
+ // Real-time validation for specific fields
+ if (field === 'tenantId' || field === 'clientId' || field === 'clientSecret') {
+ validateField(field, value);
+ }
+ };
+
+
+ const handleEnableSSO = async () => {
+ setIsEnabling(true);
+ try {
+ if (Object.keys(errors).length === 0 && organizationId) {
+ const endpoint = config.isEnabled ? 'disable' : 'enable';
+
+ await ToggleSsoStatus({
+ routeUrl: `ssoConfig/${endpoint}?organizationId=${organizationId}&provider=AzureAD`,
+ body: {},
+ });
+
+ // Toggle SSO enabled state locally
+ setConfig(prev => ({ ...prev, isEnabled: !prev.isEnabled }));
+ console.log(`SSO ${endpoint}d successfully`);
+ }
+ } catch (error) {
+ console.error('Error toggling SSO status:', error);
+ } finally {
+ setIsEnabling(false);
+ }
+ };
+
+
+ const handleSelectChange = (field: keyof SsoConfig) => (
+ event: any
+ ) => {
+ setConfig(prev => ({ ...prev, [field]: event.target.value }));
+ };
+
+
+ const handleSave = async () => {
+ setIsSaving(true);
+
+ // Validate all required fields first
+ validateField('tenantId', config.tenantId);
+ validateField('clientId', config.clientId);
+ validateField('clientSecret', config.clientSecret);
+
+ // Check if there are any validation errors
+ const hasErrors = !config.tenantId || !config.clientId || !config.clientSecret || Object.keys(errors).length > 0;
+
+ if (hasErrors) {
+ setIsSaving(false);
+ return;
+ }
+
+ try {
+ if (organizationId) {
+ await UpdateSsoConfig({
+ routeUrl: `ssoConfig?organizationId=${organizationId}&provider=AzureAD`,
+ body: {
+ client_id: config.clientId,
+ client_secret: config.clientSecret,
+ tenant_id: config.tenantId,
+ cloud_environment: config.cloudEnvironment,
+ },
+ });
+
+ // Success - could show a toast notification here
+ console.log('SSO configuration saved successfully');
+ }
+ } catch (error) {
+ console.error('Error saving SSO config:', error);
+ // Could show error toast here
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ // Card-like container styles - matching AI Trust Center spacing
+ const cardStyles = {
+ backgroundColor: theme.palette.background.paper,
+ borderRadius: theme.shape.borderRadius,
+ border: `1.5px solid ${theme.palette.divider}`,
+ padding: theme.spacing(5, 6), // 40px top/bottom, 48px left/right - same as AI Trust Center
+ boxShadow: 'none',
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Setup Guide Alert */}
+
+
+
+
+ {/* Simplified SSO Configuration Card */}
+
+
+ EntraID SSO configuration
+
+
+
+
+
+
+
+ Found in Azure Portal > Microsoft Entra ID > Overview > Tenant ID
+
+
+
+
+
+
+ Found in Azure Portal > App registrations > [Your App] > Application (client) ID
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/*
+ */}
+
+
+
+ {/* Save/Cancel Buttons for Configuration */}
+
+
+
+
+
+
+
+
+ {/* Enable SSO Card */}
+
+
+ Enable EntraID SSO
+
+
+
+
+ Enable SSO authentication for this organization. Configuration must be saved before enabling.
+
+
+
+ {config.isEnabled && config.authMethodPolicy === 'sso_only' && (
+
+ )}
+ {config.isEnabled && config.authMethodPolicy === 'both' && (
+
+ )}
+ {config.authMethodPolicy === 'password_only' && (
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default SsoConfigTab;
diff --git a/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/index.tsx b/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/index.tsx
new file mode 100644
index 0000000000..6410f22b80
--- /dev/null
+++ b/Clients/src/presentation/pages/SettingsPage/EntraIdConfig/index.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Box } from "@mui/material";
+import SsoConfigTab from "./SsoConfigTab";
+
+const EntraIdConfig: React.FC = () => {
+ return (
+
+
+
+ );
+};
+
+export default EntraIdConfig;
diff --git a/Clients/src/presentation/pages/SettingsPage/Team/index.tsx b/Clients/src/presentation/pages/SettingsPage/Team/index.tsx
index 32ea301863..c6572dd42d 100644
--- a/Clients/src/presentation/pages/SettingsPage/Team/index.tsx
+++ b/Clients/src/presentation/pages/SettingsPage/Team/index.tsx
@@ -23,6 +23,7 @@ import {
SelectChangeEvent,
TablePagination,
TableFooter,
+ Tooltip,
} from "@mui/material";
import {
UserPlus as GroupsIcon,
@@ -46,6 +47,7 @@ import {
import useUsers from "../../../../application/hooks/useUsers";
import { useAuth } from "../../../../application/hooks/useAuth";
import { UserModel } from "../../../../domain/models/Common/user/user.model";
+import { GetSsoConfig } from "../../../../application/repository/ssoConfig.repository";
interface AlertState {
variant: "success" | "info" | "warning" | "error";
@@ -137,6 +139,26 @@ const TeamManagement: React.FC = (): JSX.Element => {
const [rowsPerPage, setRowsPerPage] = useState(5); // Rows per page
const [inviteUserModalOpen, setInviteUserModalOpen] = useState(false);
+ const [isSsoEnabled, setIsSsoEnabled] = useState(false);
+ const { organizationId } = useAuth();
+
+ // Check if SSO is enabled
+ useEffect(() => {
+ const checkSsoStatus = async () => {
+ try {
+ const response = await GetSsoConfig({
+ routeUrl: `ssoConfig?organizationId=${organizationId}&provider=AzureAD`,
+ });
+ const ssoConfig = response?.data || response;
+ setIsSsoEnabled(ssoConfig?.is_enabled === true);
+ } catch (error) {
+ console.error("Failed to check SSO status:", error);
+ setIsSsoEnabled(false);
+ }
+ };
+
+ checkSsoStatus();
+ }, []);
const handleUpdateRole = useCallback(
async (memberId: string, newRole: string) => {
@@ -459,17 +481,25 @@ const TeamManagement: React.FC = (): JSX.Element => {
)}
- }
- onClick={() => inviteTeamMember()}
- />
+
+
+ }
+ onClick={() => inviteTeamMember()}
+ />
+
+
@@ -628,7 +658,7 @@ const TeamManagement: React.FC = (): JSX.Element => {
padding: "0",
},
}}
- disabled={member.id === userId}
+ disabled={member.id === userId || isSsoEnabled}
>
{roles.map((role) => (