configured tenant level login

This commit is contained in:
Kartik Sharma 2025-09-21 18:59:34 +05:30
parent aee510f527
commit b9b3788dda
9 changed files with 328 additions and 70 deletions

View File

@ -1,12 +1,19 @@
import React, { useEffect } from 'react'
import { useOrganizationModal } from './hooks/useOrganization';
import OrganizationModal from './components/Organization/OrganizationModal';
import React, { useEffect } from "react";
import { useOrganizationModal } from "./hooks/useOrganization";
import OrganizationModal from "./components/Organization/OrganizationModal";
import { useAuthModal } from "./hooks/useAuth";
import SwitchTenant from "./pages/authentication/SwitchTenant";
const ModalProvider = () => {
const { isOpen,onClose } = useOrganizationModal();
return <>{isOpen && <OrganizationModal />}</>;
const { isOpen, onClose } = useOrganizationModal();
const { isOpen: isAuthOpen } = useAuthModal();
return (
<>
{isOpen && <OrganizationModal />}
{isAuthOpen && <SwitchTenant />}
</>
);
};
export default ModalProvider
export default ModalProvider;

View File

@ -19,6 +19,7 @@ import { useProjectName } from "../../hooks/useProjects";
import eventBus from "../../services/eventBus";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_PROJECT } from "../../utils/constants";
import { useAuthModal } from "../../hooks/useAuth";
const Header = () => {
const { profile } = useProfile();
@ -26,6 +27,7 @@ const Header = () => {
const dispatch = useDispatch();
const { data, loading } = useMaster();
const navigate = useNavigate();
const {onOpen} = useAuthModal()
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
// {
// console.log(location.pathname);
@ -455,6 +457,16 @@ const Header = () => {
<span className="align-middle">Change Password</span>
</a>
</li>
<li onClick={()=>onOpen()}>
{" "}
<a
className="dropdown-item cusor-pointer"
>
<i className="bx bx-transfer-alt me-2"></i>
<span className="align-middle">Switch Tenant</span>
</a>
</li>
<li>
<div className="dropdown-divider"></div>
</li>

View File

@ -1,39 +1,51 @@
import { useState, useEffect, useCallback } from "react";
import {
Mutation,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
import AuthRepository from "../repositories/AuthRepository.jsx";
import { useDispatch, useSelector } from "react-redux";
import {
closeAuthModal,
openAuthModal,
} from "../slices/localVariablesSlice.jsx";
export const useTenantList = ({ autoFetch = true } = {}) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchTenantList = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = setData(response.data || []);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (autoFetch) fetchTenantList();
}, [autoFetch, fetchTenantList]);
return {
data,
loading,
error,
refetch: fetchTenantList, // manual trigger
};
export const useTenants = () => {
return useQuery({
queryKey: ["tenantlist"],
queryFn: async () => await AuthRepository.getTenantList(),
});
};
export const useSelectTenant = (onSuccessCallBack) => {
const queryClient = useQueryClient();
export const useTenants =()=>{
get
return useQueery({
queryKey:["tenantlist",localhost.get("orgJwtToken")],
queryFun:async()=> await AuthRepository.
})
}
return useMutation({
mutationFn: async (tenantId) => {
const res = await AuthRepository.selectTenant(tenantId);
return res.data;
},
onSuccess: (data) => {
localStorage.setItem("ltkn", data.token);
localStorage.setItem("rtkn", data.refreshToken);
if (onSuccessCallBack) onSuccessCallBack();
},
onError: (error) => {
showToast(error.message || "Error while creating project", "error");
},
});
};
export const useAuthModal = () => {
const dispatch = useDispatch();
const { isOpen } = useSelector((state) => state.localVariables.AuthModal);
return {
isOpen,
onOpen: () => dispatch(openAuthModal()),
onClose: () => dispatch(closeAuthModal()),
};
};

View File

@ -44,7 +44,7 @@ const LoginPage = () => {
localStorage.setItem("jwtToken", response.data.token);
localStorage.setItem("refreshToken", response.data.refreshToken);
setLoading(false);
navigate("/dashboard");
navigate("/auth/switch/org");
} else {
await AuthRepository.sendOTP({ email: data.username });
showToast("OTP has been sent to your email.", "success");

View File

@ -0,0 +1,101 @@
import React, { useState } from "react";
import Modal from "../../components/common/Modal";
import { useAuthModal, useSelectTenant, useTenants } from "../../hooks/useAuth";
import { useProfile } from "../../hooks/useProfile";
import { useQueryClient } from "@tanstack/react-query";
import AuthRepository from "../../repositories/AuthRepository";
const SwitchTenant = () => {
const queryClient = useQueryClient();
const { profile } = useProfile();
const [pendingTenant, setPendingTenant] = useState(null);
const { isOpen, onClose, onOpen } = useAuthModal();
const { data, isLoading, isError, error } = useTenants();
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
onClose();
queryClient.clear();
// 2. Force fetch profile fresh for the new tenant
queryClient.fetchQuery({
queryKey: ["profile"],
queryFn: () => AuthRepository.profile(),
});
});
const currentTenant = localStorage.getItem("ctnt");
const handleTenantselection = (tenantId) => {
setPendingTenant(tenantId);
localStorage.setItem("ctnt", tenantId);
chooseTenant(tenantId);
};
const contentBody = (
<div className="container text-black">
<p className=" fs-5">Switch Workplace</p>
<div className="row justify-content-center g-4">
{data?.data.map((tenant) => (
<div key={tenant.id} className="col-12 ">
<div
className={`d-flex flex-column flex-md-row gap-3 align-items-center align-items-md-start p-1 border ${
currentTenant === tenant.id ? "border-primary" : ""
} `}
>
<div
className="flex-shrink-0 text-center"
style={{
width: "80px",
aspectRatio: "1 / 1",
overflow: "hidden",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<img
src={tenant?.logoImage || "/assets/img/SP-Placeholdeer.svg"}
alt={tenant.name}
className="img-fluid"
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
<div className="d-flex flex-column text-start gap-2">
<p className="fs-5 text-muted fw-semibold mb-1">
{tenant?.name}
</p>
<div className="d-flex flex-wrap gap-2 align-items-center">
<p className="fw-semibold m-0">Industry:</p>
<p className="m-0">
{tenant?.industry?.name || "Not Available"}
</p>
</div>
{tenant?.description && (
<p className="text-start text-wrap m-0">
{tenant?.description}
</p>
)}
<button
className="btn btn-primary btn-sm mt-2 align-self-start"
onClick={() => handleTenantselection(tenant?.id)}
disabled={isPending && pendingTenant === tenant.id}
>
{isPending && pendingTenant === tenant.id
? "Please Wait.."
: "Go To Dashboard"}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
return <Modal isOpen={isOpen} onClose={onClose} body={contentBody} />;
};
export default SwitchTenant;

View File

@ -0,0 +1,102 @@
import { useEffect, useState } from "react";
import { useTenants, useSelectTenant } from "../../hooks/useAuth.jsx";
import { Link, useNavigate } from "react-router-dom";
import Dashboard from "../../components/Dashboard/Dashboard.jsx";
const TenantSelectionPage = () => {
const [pendingTenant, setPendingTenant] = useState(null);
const navigate = useNavigate();
const { data, isLoading, isError, error } = useTenants();
const { mutate: chooseTenant, isPending } = useSelectTenant(() => {
navigate("/dashboard");
});
const handleTenantselection = (tenantId) => {
setPendingTenant(tenantId);
localStorage.setItem("ctnt", tenantId);
chooseTenant(tenantId);
};
useEffect(() => {
if (localStorage.getItem("ctnt")) {
return navigate("/dashboard");
}
}, []);
if (isLoading) {
return (
<div className="container-fluid py-5 text-center">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading tenants...</span>
</div>
</div>
);
}
return (
<div className="container-fluid py-4">
{/* Header */}
<div className="text-center mb-4">
<p className="fs-4 mb-1">Welcome</p>
<p className="fs-5">
Please select which dashboard you want to explore!!!
</p>
</div>
{/* Card Section */}
<div className="row justify-content-center g-4 m-auto">
{data?.data.map((tenant) => (
<div key={tenant.id} className="col-12 col-md-10 col-lg-8">
<div className="d-flex flex-column flex-md-row gap-5 align-items-center align-items-md-start p-3 border rounded shadow-sm bg-white h-100">
{/* Image */}
<div className="flex-shrink-0 text-center">
<img
src={tenant?.logoImage || "/assets/img/SP-Placeholdeer.svg"}
alt={tenant.name}
className="img-fluid"
style={{
maxWidth: "140px",
height: "auto",
aspectRatio: "3 / 2",
}}
/>
</div>
{/* Content */}
<div className="d-flex flex-column text-start gap-2">
<p className="fs-5 text-muted fw-semibold mb-1">
{tenant?.name}
</p>
<div className="d-flex flex-wrap gap-2 align-items-center">
<p className="fw-semibold m-0">Industry:</p>
<p className="m-0">
{tenant?.industry?.name || "Not Available"}
</p>
</div>
{tenant?.description && (
<p className="text-start text-wrap m-0">
{tenant?.description}
</p>
)}
<button
className="btn btn-primary btn-sm mt-2 align-self-start"
onClick={() => handleTenantselection(tenant?.id)}
disabled={pendingTenant === tenant.id && isPending}
>
{isPending && pendingTenant === tenant.id
? "Please Wait.."
: "Go To Dashboard"}
</button>
</div>
</div>
</div>
))}
</div>
</div>
);
};
export default TenantSelectionPage;

View File

@ -15,7 +15,9 @@ const AuthRepository = {
logout: (data) => api.post("/api/auth/logout", data),
profile: () => api.get("/api/user/profile"),
changepassword: (data) => api.post("/api/auth/change-password", data),
appmenu:()=>api.get('/api/appmenu/get/menu')
appmenu: () => api.get('/api/appmenu/get/menu'),
selectTenant: (tenantId) => api.post(`/api/Auth/select-tenant/${tenantId}`),
getTenantList: () => api.get("/api/Auth/get/user/tenants"),
};

View File

@ -51,6 +51,7 @@ import { Navigate } from "react-router-dom";
import CreateTenant from "../pages/Tenant/CreateTenant";
import OrganizationPage from "../pages/Organization/OrganizationPage";
import LandingPage from "../pages/Home/LandingPage";
import TenantSelectionPage from "../pages/authentication/TenantSelectionPage";
const router = createBrowserRouter(
[
{
@ -69,6 +70,7 @@ const router = createBrowserRouter(
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
],
},
{ path: "/auth/switch/org", element: <TenantSelectionPage /> },
{
element: <ProtectedRoute />,
errorElement: <ErrorPage />,

View File

@ -3,54 +3,56 @@ import { createSlice } from "@reduxjs/toolkit";
const localVariablesSlice = createSlice({
name: "localVariables",
initialState: {
selectedMaster:"Application Role",
regularizationCount:0,
defaultDateRange: {
selectedMaster: "Application Role",
regularizationCount: 0,
defaultDateRange: {
startDate: null,
endDate: null,
},
projectId: null,
reload:false,
reload: false,
OrganizationModal:{
OrganizationModal: {
isOpen: false,
orgData: null,
prevStep:null,
prevStep: null,
startStep: 1,
flowType: "default",
}
flowType: "default",
},
AuthModal: {
isOpen: false,
},
},
reducers: {
changeMaster: (state, action) => {
state.selectedMaster = action.payload;
state.selectedMaster = action.payload;
},
updateRegularizationCount: (state, action) => {
state.regularizationCount = action.payload;
},
setProjectId: (state, action) => {
localStorage.setItem("project",null)
setProjectId: (state, action) => {
localStorage.setItem("project", null);
state.projectId = action.payload;
localStorage.setItem("project",state.projectId || null)
localStorage.setItem("project", state.projectId || null);
},
refreshData: ( state, action ) =>
{
state.reload = action.payload
refreshData: (state, action) => {
state.reload = action.payload;
},
setDefaultDateRange: (state, action) => {
state.defaultDateRange = action.payload;
},
openOrgModal: (state, action) => {
state.OrganizationModal.isOpen = true;
state.OrganizationModal.orgData = action.payload?.orgData || null;
openOrgModal: (state, action) => {
state.OrganizationModal.isOpen = true;
state.OrganizationModal.orgData = action.payload?.orgData || null;
if (state.OrganizationModal.startStep) {
state.OrganizationModal.prevStep = state.OrganizationModal.startStep;
}
if (state.OrganizationModal.startStep) {
state.OrganizationModal.prevStep = state.OrganizationModal.startStep;
}
state.OrganizationModal.startStep = action.payload?.startStep || 1;
state.OrganizationModal.flowType = action.payload?.flowType || "default";
state.OrganizationModal.startStep = action.payload?.startStep || 1;
state.OrganizationModal.flowType = action.payload?.flowType || "default";
},
closeOrgModal: (state) => {
state.OrganizationModal.isOpen = false;
@ -61,8 +63,26 @@ state.OrganizationModal.isOpen = true;
toggleOrgModal: (state) => {
state.OrganizationModal.isOpen = !state.OrganizationModal.isOpen;
},
openAuthModal: (state, action) => {
state.AuthModal.isOpen = true;
},
closeAuthModal: (state, action) => {
state.AuthModal.isOpen = false;
},
},
});
export const { changeMaster ,updateRegularizationCount,setProjectId,refreshData,setDefaultDateRange,openOrgModal,closeOrgModal,toggleOrgModal} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;
export const {
changeMaster,
updateRegularizationCount,
setProjectId,
refreshData,
setDefaultDateRange,
openOrgModal,
closeOrgModal,
toggleOrgModal,
openAuthModal,
closeAuthModal,
} = localVariablesSlice.actions;
export default localVariablesSlice.reducer;