Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e3371a6b | ||
|
|
796f807539 | ||
|
|
b4e887bc60 | ||
|
|
ad47431581 | ||
|
|
136644966b | ||
|
|
99c6487c38 | ||
|
|
2192206e43 | ||
|
|
32df114ee5 | ||
|
|
3ef3200221 | ||
|
|
3f9cdb4a43 | ||
|
|
6b5ab22609 | ||
|
|
ddb29f25e4 | ||
|
|
2f0150c92b | ||
|
|
d64bd3d7f7 |
@@ -4,4 +4,5 @@ VITE_PRODUCT_HUB_API_URL=https://product-hub.maskantech.in/api/v1
|
||||
VITE_DOCUSEAL_URL=https://docuseal.com
|
||||
VITE_DOCUSEAL_SCRIPT_URL=https://cdn.docuseal.com/js/form.js
|
||||
VITE_PDF_EDITOR_DOCKER_URL=https://pdf-dev.maskantech.in/
|
||||
VITE_SAAS_API_URL=https://saas-test-api.maskantech.in
|
||||
VITE_SAAS_API_URL=https://saas-test.maskantech.in
|
||||
VITE_LANDING_PAGE_URL=https://docqube-test.maskantech.in
|
||||
+20
-1
@@ -36,12 +36,31 @@ import ConversionWorkflow from '@/features/conversion/ConversionWorkflow';
|
||||
import UpgradePlan from '@/features/subscription/SubscriptionPage';
|
||||
|
||||
|
||||
import { useAuth } from '@/features/auth/context';
|
||||
|
||||
const RouteFallback = () => (
|
||||
<div className="flex min-h-[60vh] items-center justify-center" role="status" aria-live="polite">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-transparent" />
|
||||
<span className="sr-only">Loading</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const TenantDashboardRoute = ({ children }: { children: React.ReactElement }) => {
|
||||
const { user, isLoading } = useAuth();
|
||||
if (isLoading) {
|
||||
return <RouteFallback />;
|
||||
}
|
||||
const isSuperadmin = Boolean(
|
||||
user?.is_superadmin ||
|
||||
user?.role === 'superadmin' ||
|
||||
user?.accesses?.includes('superadmin.main.view')
|
||||
);
|
||||
if (isSuperadmin) {
|
||||
return <Navigate to="/admin/dashboard" replace />;
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
// Scroll to top component
|
||||
const ScrollToTop = () => {
|
||||
const { pathname } = useLocation();
|
||||
@@ -157,7 +176,7 @@ function AppContent() {
|
||||
</RequireAuth>
|
||||
}
|
||||
>
|
||||
<Route path="/dashboard" element={<DashboardMainView />} />
|
||||
<Route path="/dashboard" element={<TenantDashboardRoute><DashboardMainView /></TenantDashboardRoute>} />
|
||||
<Route path="/documents" element={<RequireAccess accessCode="document.read"><MyDocumentsView /></RequireAccess>} />
|
||||
<Route
|
||||
path="/new-conversion"
|
||||
|
||||
@@ -14,7 +14,11 @@ const PublicRoute: React.FC<{ children: React.ReactElement }> = ({ children }) =
|
||||
}
|
||||
|
||||
if (isAuthenticated) {
|
||||
if (user?.accesses?.includes('superadmin.main.view')) {
|
||||
if (
|
||||
user?.is_superadmin ||
|
||||
user?.role === 'superadmin' ||
|
||||
user?.accesses?.includes('superadmin.main.view')
|
||||
) {
|
||||
return <Navigate to="/admin/dashboard" replace />;
|
||||
}
|
||||
return <Navigate to="/dashboard" replace />;
|
||||
|
||||
@@ -76,6 +76,7 @@ export const authAPI = {
|
||||
saasSignInRes = await fetch(`${saasApiUrl}/api/auth/signin`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ email: credentials.email.trim(), password: credentials.password }),
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
||||
@@ -38,20 +38,11 @@ const LoginPage: React.FC = () => {
|
||||
// Instant auth state update without hard page reload
|
||||
await checkAuthStatus();
|
||||
|
||||
let isSuperadmin = Boolean(ssoSuccess.user?.is_superadmin);
|
||||
try {
|
||||
const profileRes = await fetch('/api/me/profile', { credentials: 'include' });
|
||||
if (profileRes.ok) {
|
||||
const profile = await profileRes.json();
|
||||
if (
|
||||
profile.accesses?.includes('superadmin.main.view') ||
|
||||
profile.role === 'superadmin' ||
|
||||
profile.is_superadmin
|
||||
) {
|
||||
isSuperadmin = true;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
let isSuperadmin = Boolean(
|
||||
ssoSuccess.user?.is_superadmin ||
|
||||
ssoSuccess.user?.role === 'superadmin' ||
|
||||
(Array.isArray(ssoSuccess.user?.permissions) && ssoSuccess.user.permissions.includes('superadmin.main.view'))
|
||||
);
|
||||
|
||||
if (isSuperadmin) {
|
||||
navigate('/admin/dashboard', { replace: true });
|
||||
|
||||
@@ -21,6 +21,7 @@ interface User {
|
||||
role: string | null;
|
||||
roles: RoleRef[];
|
||||
accesses: string[];
|
||||
is_superadmin?: boolean;
|
||||
subscription_details?: {
|
||||
plan_id?: string | null;
|
||||
plan_name?: string | null;
|
||||
@@ -143,6 +144,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
role: profile.role || null,
|
||||
roles: profile.roles || [],
|
||||
accesses: profile.accesses || [],
|
||||
is_superadmin: Boolean(profile.is_superadmin || profile.role === 'superadmin' || profile.accesses?.includes('superadmin.main.view')),
|
||||
subscription_details: profile.subscription_details || null,
|
||||
preferred_language: getSafeLanguage(profile.preferred_language),
|
||||
terms_accepted: profile.terms_accepted ?? false,
|
||||
@@ -174,6 +176,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
role: profile.role || null,
|
||||
roles: profile.roles || [],
|
||||
accesses: profile.accesses || [],
|
||||
is_superadmin: Boolean(profile.is_superadmin || profile.role === 'superadmin' || profile.accesses?.includes('superadmin.main.view')),
|
||||
subscription_details: profile.subscription_details || null,
|
||||
preferred_language: getSafeLanguage(profile.preferred_language),
|
||||
terms_accepted: profile.terms_accepted ?? false,
|
||||
|
||||
@@ -93,22 +93,22 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folde
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4 sm:p-6 animate-fade-in">
|
||||
{}
|
||||
<div className="fixed inset-0 z-[150] flex items-center justify-center p-3 sm:p-4 animate-fade-in">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-slate-950/40 backdrop-blur-md transition-all duration-500"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{}
|
||||
{/* Modal Card */}
|
||||
<div
|
||||
className="relative w-full max-w-lg bg-white rounded-3xl shadow-[0_32px_64px_-12px_rgba(0,0,0,0.14)] overflow-hidden flex flex-col transform transition-all duration-300 scale-100 border border-slate-100/50"
|
||||
style={{ maxHeight: '80vh' }}
|
||||
className="relative w-full max-w-lg bg-white rounded-3xl shadow-[0_32px_64px_-12px_rgba(0,0,0,0.14)] overflow-hidden flex flex-col transform transition-all duration-300 scale-100 border border-slate-100/50 my-auto"
|
||||
style={{ maxHeight: 'calc(100vh - 2rem)' }}
|
||||
>
|
||||
{}
|
||||
<div className="flex items-center justify-between px-8 py-5 border-b border-slate-50 bg-white/80 backdrop-blur-md z-10">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-3.5 border-b border-slate-100 bg-white/80 backdrop-blur-md z-10 shrink-0">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-900 tracking-tight">Upload Files</h3>
|
||||
<h3 className="text-base font-bold text-slate-900 tracking-tight">Upload Files</h3>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Secure Document Gateway</p>
|
||||
<span className="w-1 h-1 bg-slate-300 rounded-full" />
|
||||
@@ -117,89 +117,70 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folde
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`group p-2 rounded-xl transition-all duration-300 hover:bg-slate-50 text-slate-400 hover:text-slate-900`}
|
||||
className="group p-1.5 rounded-xl transition-all duration-300 hover:bg-slate-50 text-slate-400 hover:text-slate-900"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="flex-1 overflow-y-auto px-8 py-6 space-y-6 scrollbar-hide">
|
||||
{}
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-3.5 space-y-3 min-h-0">
|
||||
{/* Dropzone */}
|
||||
<div
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={onDrop}
|
||||
className={`group p-8 rounded-xl border-2 border-dashed transition-all duration-500 flex flex-col items-center justify-center gap-4 relative overflow-hidden
|
||||
className={`group p-4 rounded-2xl border-2 border-dashed transition-all duration-300 flex flex-col items-center justify-center gap-2 relative overflow-hidden
|
||||
${isDragging
|
||||
? 'border-[#3b82f6] bg-blue-50/30 scale-[0.99] shadow-inner'
|
||||
: 'border-[#e2e8f0] bg-white hover:bg-slate-50/50 hover:border-[#cbd5e1]'}
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-center -space-x-3 mb-2">
|
||||
<div className="w-12 h-14 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[-15deg] shadow-sm">
|
||||
<div className="w-6 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-6 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-4 h-0.5 bg-blue-300"></div>
|
||||
<div className="flex justify-center -space-x-2 mb-0.5">
|
||||
<div className="w-8 h-10 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[-15deg] shadow-sm">
|
||||
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-3 h-0.5 bg-blue-300"></div>
|
||||
</div>
|
||||
<div className="w-14 h-16 bg-blue-500 rounded border-2 border-white flex flex-col items-center justify-center z-10 shadow-md">
|
||||
<div className="w-8 h-0.5 bg-blue-200 mb-1.5"></div>
|
||||
<div className="w-8 h-0.5 bg-blue-200 mb-1.5"></div>
|
||||
<div className="w-6 h-0.5 bg-blue-200"></div>
|
||||
<div className="w-9 h-11 bg-blue-500 rounded border-2 border-white flex flex-col items-center justify-center z-10 shadow-md">
|
||||
<div className="w-5 h-0.5 bg-blue-200 mb-1"></div>
|
||||
<div className="w-5 h-0.5 bg-blue-200 mb-1"></div>
|
||||
<div className="w-3.5 h-0.5 bg-blue-200"></div>
|
||||
</div>
|
||||
<div className="w-12 h-14 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[15deg] shadow-sm">
|
||||
<div className="w-6 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-6 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-4 h-0.5 bg-blue-300"></div>
|
||||
<div className="w-8 h-10 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[15deg] shadow-sm">
|
||||
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
|
||||
<div className="w-3 h-0.5 bg-blue-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center space-y-1">
|
||||
<h3 className="text-[16px] font-bold text-slate-800">Drag and drop files here</h3>
|
||||
<p className="text-[12px] text-slate-400 font-medium">Supported formats: .pdf, .doc, .docx, .xls, .xlsx, .txt, .csv, .jpg, .jpeg, .png, .gif, .webp, .svg</p>
|
||||
<p className="text-[12px] text-slate-400 font-medium">Maximum file size: 30MB</p>
|
||||
<div className="text-center space-y-0.5">
|
||||
<h3 className="text-sm font-bold text-slate-800">Drag and drop files here</h3>
|
||||
<p className="text-[11px] text-slate-400 font-medium max-w-xs leading-relaxed">Supported formats: .pdf, .doc, .docx, .xls, .xlsx, .txt, .csv, .jpg, .jpeg, .png, .gif, .webp, .svg</p>
|
||||
<p className="text-[10px] text-slate-400 font-semibold">Maximum file size: 30MB</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full relative flex items-center justify-center my-4">
|
||||
<div className="w-full relative flex items-center justify-center my-0.5">
|
||||
<div className="absolute w-full border-t border-dashed border-slate-200"></div>
|
||||
<span className="bg-white px-3 text-xs text-slate-400 relative z-10">or</span>
|
||||
<span className="bg-white px-3 text-[10px] uppercase font-bold text-slate-400 relative z-10">or</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 w-full justify-center">
|
||||
<div className="flex gap-3 w-full justify-center">
|
||||
{/* My Computer */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex flex-col items-center justify-center gap-3 w-28 h-24 rounded-lg hover:bg-slate-50 transition-colors group"
|
||||
className="flex items-center justify-center gap-2 px-4 py-2 rounded-xl border border-slate-200/80 bg-slate-50 hover:bg-blue-50 hover:border-blue-300 transition-all duration-200 group cursor-pointer shadow-xs"
|
||||
>
|
||||
<div className="w-12 h-12 flex items-center justify-center group-hover:scale-105 transition-transform">
|
||||
<svg viewBox="0 0 24 24" fill="none" className="w-10 h-10 text-blue-500">
|
||||
<path d="M4 6h16v10H4z" fill="#3b82f6" />
|
||||
<path d="M3 5a2 2 0 012-2h14a2 2 0 012 2v12a2 2 0 01-2 2H3a2 2 0 01-2-2V5z" fill="#1e40af" />
|
||||
<path d="M8 21h8v-2H8v2z" fill="#94a3b8" />
|
||||
<path d="M4 6h16v10H4z" fill="#bfdbfe" />
|
||||
<div className="w-6 h-6 flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<svg viewBox="0 0 24 24" fill="none" className="w-5 h-5 text-blue-600" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-slate-700">My Computer</span>
|
||||
<span className="text-xs font-bold text-slate-700 group-hover:text-blue-600 transition-colors">Browse from Computer</span>
|
||||
</button>
|
||||
|
||||
{/* Google Drive button (Commented out) */}
|
||||
{/*
|
||||
<button
|
||||
onClick={() => openDrivePicker()}
|
||||
disabled={isDownloading}
|
||||
className={`flex flex-col items-center justify-center gap-3 w-28 h-24 rounded-lg hover:bg-slate-50 transition-colors group ${isDownloading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div className="w-12 h-12 flex items-center justify-center group-hover:scale-105 transition-transform">
|
||||
{isDownloading ? (
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||
) : (
|
||||
<img src={GoogleDriveIcon} alt="Google Drive" className="w-9 h-9" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] font-semibold text-slate-700">Google Drive</span>
|
||||
</button>
|
||||
*/}
|
||||
|
||||
</div>
|
||||
|
||||
<input
|
||||
@@ -212,7 +193,7 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folde
|
||||
/>
|
||||
</div>
|
||||
|
||||
{}
|
||||
{/* Upload Queue Section */}
|
||||
{uploads.length > 0 && (
|
||||
<div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
@@ -288,8 +269,8 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folde
|
||||
)}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="px-8 py-5 border-t border-slate-50 bg-slate-50/30 flex items-center justify-between">
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-3.5 border-t border-slate-100 bg-slate-50/50 flex items-center justify-between shrink-0">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[9px] text-slate-400 font-black uppercase tracking-widest mb-0.5">Status Report</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -298,18 +279,19 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folde
|
||||
{uploads.length === 0 ? 'Awaiting Input' : `${uploads.filter(u => u.status === 'completed' || u.status === 'ready').length} of ${uploads.length} Secured`}
|
||||
</span>
|
||||
</div>
|
||||
</div> <button
|
||||
</div>
|
||||
<button
|
||||
onClick={() => uploads.length > 0 && handleFinish()}
|
||||
disabled={activeCount > 0 || hasVirus}
|
||||
className={`px-6 py-2.5 text-[10px] font-black rounded-xl transition-all duration-500 tracking-[0.1em] flex items-center gap-2
|
||||
className={`px-5 py-2 text-[10px] font-black rounded-xl transition-all duration-500 tracking-[0.1em] flex items-center gap-2
|
||||
${(uploads.length > 0 && activeCount === 0)
|
||||
? 'bg-emerald-600 text-white hover:bg-emerald-700 hover:-translate-y-1 shadow-2xl shadow-emerald-200'
|
||||
? 'bg-emerald-600 text-white hover:bg-emerald-700 hover:-translate-y-0.5 shadow-lg shadow-emerald-200'
|
||||
: activeCount > 0
|
||||
? 'bg-slate-100 text-slate-300 cursor-not-allowed scale-95'
|
||||
: hasVirus
|
||||
? 'bg-rose-100 text-rose-400 cursor-not-allowed border border-rose-200'
|
||||
: uploads.length > 0
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700 shadow-2xl shadow-blue-200 hover:-translate-y-1'
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200 hover:-translate-y-0.5'
|
||||
: 'bg-slate-200 text-slate-500 hover:bg-slate-300'}
|
||||
`}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import ConfirmationModal from '@/components/custom/CustomConfirmationModal';
|
||||
import { tenantApi, Tenant } from '@/services/tenant';
|
||||
import { useAuth } from '@/features/auth/context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSaasFrontendUrl } from '@/lib/saasConfig';
|
||||
|
||||
const RoleManagement = () => {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
@@ -77,12 +78,17 @@ const RoleManagement = () => {
|
||||
}, []);
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
if (isTenantAdmin) {
|
||||
const saasUrl = getSaasFrontendUrl();
|
||||
window.open(`${saasUrl}/roles/add`, '_blank');
|
||||
return;
|
||||
}
|
||||
setIsEditing(false);
|
||||
setIsViewModalOpen(false);
|
||||
setRoleName('');
|
||||
setRoleDescription('');
|
||||
setSelectedAccessIds([]);
|
||||
setSelectedTenantId(isTenantAdmin ? user!.tenant_id! : '');
|
||||
setSelectedTenantId('');
|
||||
setIsSuperadminRole(false);
|
||||
setIsDefaultRole(false);
|
||||
setIsModalOpen(true);
|
||||
@@ -261,7 +267,7 @@ const RoleManagement = () => {
|
||||
const isDefault = row.is_default;
|
||||
|
||||
const isGlobalSystemRole = !row.tenant_id;
|
||||
const canModify = isCurrentUserSuperadmin || (!isDefault && !isGlobalSystemRole);
|
||||
const canModify = !isTenantAdmin && (isCurrentUserSuperadmin || (!isDefault && !isGlobalSystemRole));
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
@@ -274,7 +280,7 @@ const RoleManagement = () => {
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</CustomButton>
|
||||
{!isSuperadminRole && (
|
||||
{!isTenantAdmin && !isSuperadminRole && (
|
||||
<>
|
||||
<CustomButton
|
||||
variant="text"
|
||||
@@ -321,6 +327,22 @@ const RoleManagement = () => {
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{isTenantAdmin && (
|
||||
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg flex flex-wrap items-center justify-between gap-2 text-sm text-blue-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Role creation, editing, and deletion are centrally managed in SaaS Hub.</span>
|
||||
</div>
|
||||
<a
|
||||
href={`${getSaasFrontendUrl()}/roles`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold underline hover:text-blue-900 flex items-center gap-1"
|
||||
>
|
||||
Manage in SaaS Hub →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<CustomTable
|
||||
data={roles}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { tenantApi, Tenant } from '@/services/tenant';
|
||||
import { ConfigurationApi, LimitsSnapshot } from '@/features/superAdmin/settings/ConfigurationApi';
|
||||
import { useAuth } from '@/features/auth/context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSaasFrontendUrl } from '@/lib/saasConfig';
|
||||
|
||||
export default function UserManagement() {
|
||||
const [users, setUsers] = useState<UserListItem[]>([]);
|
||||
@@ -166,6 +167,12 @@ export default function UserManagement() {
|
||||
|
||||
|
||||
const openCreateModal = async () => {
|
||||
if (isTenantAdmin) {
|
||||
const saasUrl = getSaasFrontendUrl();
|
||||
window.open(`${saasUrl}/users/add`, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
setSelectedUser(null);
|
||||
setFormName('');
|
||||
@@ -180,14 +187,10 @@ export default function UserManagement() {
|
||||
|
||||
refreshLimits();
|
||||
|
||||
if (isTenantAdmin) {
|
||||
await onTenantChanged(user!.tenant_id!);
|
||||
} else {
|
||||
setFormTenantId('');
|
||||
setSelectedTenantActiveUsers(null);
|
||||
setSelectedTenantUserLimit(null);
|
||||
setAvailableRoles([]);
|
||||
}
|
||||
setFormTenantId('');
|
||||
setSelectedTenantActiveUsers(null);
|
||||
setSelectedTenantUserLimit(null);
|
||||
setAvailableRoles([]);
|
||||
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
@@ -702,6 +705,22 @@ export default function UserManagement() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTenantAdmin && (
|
||||
<div className="mb-6 p-3 bg-blue-50 border border-blue-200 rounded-lg flex flex-wrap items-center justify-between gap-2 text-sm text-blue-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>User creation, editing, and deletion are centrally managed in SaaS Hub.</span>
|
||||
</div>
|
||||
<a
|
||||
href={`${getSaasFrontendUrl()}/users`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-semibold underline hover:text-blue-900 flex items-center gap-1"
|
||||
>
|
||||
Manage in SaaS Hub →
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(orgCreditStats || isLoading) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
{isLoading && !orgCreditStats ? (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Helper to dynamically resolve the Central SaaS Management Frontend URL
|
||||
* Supports local development, test environments, and production dynamically.
|
||||
*/
|
||||
export function getSaasFrontendUrl(): string {
|
||||
const envSaasFrontend = (import.meta.env.VITE_SAAS_FRONTEND_URL as string | undefined);
|
||||
if (envSaasFrontend && envSaasFrontend.trim()) {
|
||||
return envSaasFrontend.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('maskantech.in')) {
|
||||
if (hostname.includes('test')) {
|
||||
return 'https://saas-test.maskantech.in';
|
||||
}
|
||||
return 'https://saas.maskantech.in';
|
||||
}
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return 'http://localhost:5174';
|
||||
}
|
||||
}
|
||||
|
||||
const saasApi = (import.meta.env.VITE_SAAS_API_URL as string | undefined) || '';
|
||||
if (saasApi.includes('8000') || saasApi.includes('localhost')) {
|
||||
return 'http://localhost:5174';
|
||||
}
|
||||
if (saasApi.trim()) {
|
||||
return saasApi.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
return 'http://localhost:5173';
|
||||
}
|
||||
Reference in New Issue
Block a user