751 lines
43 KiB
TypeScript
751 lines
43 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useOmni } from "@/components/OmniSyncProvider";
|
|
|
|
interface Tenant {
|
|
id: string;
|
|
name: string;
|
|
licenseNumber?: string;
|
|
licenses?: string | Record<string, string>;
|
|
brandColor?: string;
|
|
platformName?: string;
|
|
securityTier?: string;
|
|
}
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
licenses?: string | Record<string, string>;
|
|
byokEnabled?: boolean;
|
|
byokKey?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface Feature {
|
|
key: string;
|
|
name: string;
|
|
module?: string;
|
|
}
|
|
|
|
interface Package {
|
|
id: string;
|
|
name: string;
|
|
price: string;
|
|
features: string;
|
|
}
|
|
|
|
export default function AdminDashboard() {
|
|
const {} = useOmni();
|
|
const [tenant, setTenant] = useState<Tenant | null>(null);
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [systemFeatures, setSystemFeatures] = useState<Feature[]>([]);
|
|
const [activeTab, setActiveTab] = useState('users');
|
|
const [debugError, setDebugError] = useState<string | null>(null);
|
|
|
|
const [showAddUserModal, setShowAddUserModal] = useState(false);
|
|
const [showLimitsModal, setShowLimitsModal] = useState(false);
|
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
const [userLicenses, setUserLicenses] = useState<Record<string, string>>({});
|
|
|
|
const [newEmail, setNewEmail] = useState("");
|
|
const [newPassword, setNewPassword] = useState("");
|
|
const [packages, setPackages] = useState<Package[]>([]);
|
|
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
|
|
|
// BYOK States
|
|
const [orgByokEnabled, setOrgByokEnabled] = useState(false);
|
|
const [orgByokKey, setOrgByokKey] = useState("");
|
|
const [userByokEnabled, setUserByokEnabled] = useState(false);
|
|
const [userByokKey, setUserByokKey] = useState("");
|
|
|
|
// Quantum Toast System
|
|
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null);
|
|
|
|
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
|
setToast({ message, type });
|
|
setTimeout(() => setToast(null), 4000);
|
|
};
|
|
|
|
const router = useRouter();
|
|
|
|
const handleLogout = async () => {
|
|
try {
|
|
await fetch('/api/auth/logout', { method: 'POST' });
|
|
window.location.href = '/';
|
|
} catch (e) {
|
|
console.error("Logout failed", e);
|
|
window.location.href = '/';
|
|
}
|
|
};
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const resp = await fetch("/api/admin", { cache: "no-store" });
|
|
const data = await resp.json();
|
|
if (data.error) {
|
|
window.location.href = window.location.pathname.replace('/admin', '/dashboard');
|
|
} else {
|
|
setTenant(data.tenant);
|
|
setOrgByokEnabled(data.tenant.byokEnabled || false);
|
|
setOrgByokKey(data.tenant.byokKey || "");
|
|
setUsers(data.users);
|
|
setSystemFeatures(data.systemFeatures || []);
|
|
|
|
// Fetch Packages for Billing
|
|
const pResp = await fetch("/api/superadmin/packages");
|
|
const pData = await pResp.json();
|
|
setPackages(pData.packages || []);
|
|
}
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
setDebugError(e.message || "Unknown error occurred");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const init = async () => {
|
|
await fetchData();
|
|
};
|
|
init();
|
|
|
|
}, []);
|
|
|
|
const handleAddUser = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
const resp = await fetch("/api/admin", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "add_user", email: newEmail, password: newPassword, role: "user" })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.success) {
|
|
setUsers([...users, data.user]);
|
|
setShowAddUserModal(false);
|
|
setNewEmail(""); setNewPassword("");
|
|
showToast("Employee added successfully.");
|
|
} else {
|
|
showToast("Failed: " + data.error, "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Server error.", "error");
|
|
}
|
|
};
|
|
|
|
const handleOpenLimits = (u: User) => {
|
|
setSelectedUser(u);
|
|
try {
|
|
let parsed: Record<string, string> = {};
|
|
if (typeof u.licenses === 'string') {
|
|
const js = JSON.parse(u.licenses);
|
|
if (Array.isArray(js)) js.forEach((k: string) => parsed[k] = "GRANTED");
|
|
else parsed = js;
|
|
} else if (u.licenses && typeof u.licenses === 'object') {
|
|
if (Array.isArray(u.licenses)) u.licenses.forEach((k: string) => parsed[k] = "GRANTED");
|
|
else parsed = u.licenses;
|
|
}
|
|
setUserLicenses(parsed);
|
|
setUserByokEnabled(u.byokEnabled || false);
|
|
setUserByokKey(u.byokKey || "");
|
|
} catch(_e) { setUserLicenses({}); }
|
|
setShowLimitsModal(true);
|
|
};
|
|
|
|
const handleSaveLimits = async () => {
|
|
try {
|
|
const resp = await fetch("/api/admin", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: "update_user_licenses",
|
|
targetUserId: selectedUser?.id,
|
|
newLicenses: userLicenses,
|
|
byokEnabled: userByokEnabled,
|
|
byokKey: userByokKey
|
|
})
|
|
});
|
|
if (resp.ok) {
|
|
showToast("User limits applied successfully.");
|
|
setShowLimitsModal(false);
|
|
fetchData();
|
|
try {
|
|
const bc = new BroadcastChannel('omni_channel');
|
|
bc.postMessage({ type: 'REFRESH_QUANTUM_TOKEN' });
|
|
} catch(e){}
|
|
} else {
|
|
const d = await resp.json();
|
|
showToast(d.error || "Error saving limits.", "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Error saving limits. Periksa koneksi Anda.", "error");
|
|
}
|
|
};
|
|
|
|
const handleSaveOrgByok = async () => {
|
|
try {
|
|
const resp = await fetch("/api/admin", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
action: "update_tenant_byok",
|
|
byokEnabled: orgByokEnabled,
|
|
byokKey: orgByokKey
|
|
})
|
|
});
|
|
if (resp.ok) {
|
|
showToast("Organizational BYOK applied successfully.");
|
|
fetchData();
|
|
} else {
|
|
const d = await resp.json();
|
|
showToast("Error: " + d.error, "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Error saving BYOK.", "error");
|
|
}
|
|
};
|
|
|
|
const handleCheckout = async (packageId: string) => {
|
|
setCheckoutLoading(true);
|
|
try {
|
|
const resp = await fetch("/api/admin/checkout", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ packageId })
|
|
});
|
|
const data = await resp.json();
|
|
if (data.url) {
|
|
window.location.assign(data.url);
|
|
} else {
|
|
showToast("Checkout Error: " + (data.error || "Unknown error"), "error");
|
|
}
|
|
} catch (_e) {
|
|
showToast("Payment gateway connection failed.", "error");
|
|
} finally {
|
|
setCheckoutLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-blue-600 font-medium">Loading Admin Workspace...</div>;
|
|
if (debugError) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-red-600 font-medium">Error: {debugError}</div>;
|
|
if (!tenant) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-red-600 font-medium">Error: Tenant not found</div>;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#f4f5f9] text-gray-900 font-sans pb-20 relative">
|
|
|
|
{/* Quantum Toast Notification System */}
|
|
{toast && (
|
|
<div className={`fixed top-20 right-8 z-[100] px-6 py-4 rounded-xl shadow-2xl backdrop-blur-xl border flex items-center gap-4 animate-in slide-in-from-right-8 fade-in duration-300 ${toast.type === 'success' ? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-700' : 'bg-red-500/10 border-red-500/30 text-red-700'}`}>
|
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${toast.type === 'success' ? 'bg-emerald-500 text-white' : 'bg-red-500 text-white'}`}>
|
|
{toast.type === 'success' ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path></svg>
|
|
) : (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div className="text-xs font-black uppercase tracking-widest opacity-80 mb-0.5">{toast.type === 'success' ? 'System Success' : 'System Alert'}</div>
|
|
<div className="text-sm font-medium">{toast.message}</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* HEADER ZOOM-LIKE */}
|
|
<header className="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-8 sticky top-0 z-40 shadow-sm">
|
|
<div className="flex items-center gap-3">
|
|
<svg className="w-7 h-7 text-blue-600" fill="currentColor" viewBox="0 0 24 24"><path d="M4 4h10a2 2 0 0 1 2 2v3.5l4-3v11l-4-3V18a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"></path></svg>
|
|
<h1 className="text-xl font-bold tracking-tight text-gray-800">Workspace Admin <span className="text-sm font-normal text-gray-500 ml-2">{tenant.name}</span></h1>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<a href="/admin/telemetry" className="px-3 py-1.5 bg-gradient-to-r from-[#25D366] to-[#0b5cff] text-white text-xs font-bold rounded-md hover:opacity-90 transition-opacity flex items-center gap-2 shadow-sm">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
TENANT COMMAND CENTER
|
|
</a>
|
|
<button onClick={() => router.push('/dashboard')} className="text-sm text-gray-600 hover:text-gray-900 font-medium transition-colors">Go to App</button>
|
|
<button onClick={handleLogout} className="text-sm text-blue-600 hover:text-blue-800 font-semibold transition-colors">Sign Out</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto px-6 py-8 flex flex-col md:flex-row gap-8">
|
|
|
|
{/* SIDEBAR */}
|
|
<div className="w-full md:w-64 shrink-0">
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-4 sticky top-24">
|
|
<div className="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3 px-2">Account Info</div>
|
|
<div className="mb-6 px-2">
|
|
<div className="font-bold text-gray-900">{tenant.name}</div>
|
|
<div className="text-xs text-gray-500 font-mono mt-1">LIC: {tenant.licenseNumber || 'PENDING'}</div>
|
|
|
|
{/* Security & Network Feature Badges */}
|
|
{(() => {
|
|
let lic: Record<string, string> = {};
|
|
try { lic = typeof tenant.licenses === 'string' ? JSON.parse(tenant.licenses || '{}') : (tenant.licenses || {}); } catch {}
|
|
const feats = [
|
|
{ key: 'tls_global_ca', label: "\ud83c\udf10 Let's Encrypt", cls: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
|
|
{ key: 'tls_sovereign', label: '\ud83d\udee1\ufe0f Private CA (X)', cls: 'bg-cyan-50 text-cyan-700 border-cyan-200' },
|
|
{ key: 'ipv6_dual_stack', label: '\ud83c\udf10 IPv6 Dual-Stack', cls: 'bg-violet-50 text-violet-700 border-violet-200' },
|
|
];
|
|
const visible = feats.filter(f => lic[f.key] && lic[f.key] !== 'HIDDEN');
|
|
if (visible.length === 0) return null;
|
|
return (
|
|
<div className="mt-3 space-y-1.5">
|
|
{visible.map(f => {
|
|
const state = lic[f.key];
|
|
if (state === 'GRANTED') return (
|
|
<div key={f.key} className={`flex items-center gap-1.5 text-[10px] font-bold px-2 py-1 rounded-md border ${f.cls}`}>
|
|
<span className="relative flex h-1.5 w-1.5"><span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span><span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500"></span></span>
|
|
{f.label}
|
|
</div>
|
|
);
|
|
if (state === 'UPSELL') return (
|
|
<button key={f.key} onClick={() => setActiveTab('billing')} className="flex items-center gap-1.5 text-[10px] font-bold px-2 py-1 rounded-md bg-amber-50 text-amber-600 border border-amber-200 hover:bg-amber-100 transition-colors w-full text-left">
|
|
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd"></path></svg>
|
|
{f.label} <span className="ml-auto text-[8px] uppercase tracking-wider">Upgrade</span>
|
|
</button>
|
|
);
|
|
return null;
|
|
})}
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
<nav className="space-y-1">
|
|
<button
|
|
onClick={() => setActiveTab('users')}
|
|
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'users' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
|
>
|
|
User Management
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('billing')}
|
|
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'billing' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
|
>
|
|
Billing & Plans
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('security')}
|
|
className={`w-full text-left px-3 py-2 rounded-md font-medium text-sm transition-all ${activeTab === 'security' ? 'bg-blue-50 text-blue-700' : 'text-gray-600 hover:bg-gray-50'}`}
|
|
>
|
|
Security & BYOK
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MAIN CONTENT */}
|
|
<div className="flex-1 space-y-6">
|
|
{activeTab === 'users' && (
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<div className="px-6 py-5 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
|
|
<h2 className="text-lg font-semibold text-gray-800">Users ({users.length})</h2>
|
|
<button
|
|
onClick={() => setShowAddUserModal(true)}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors shadow-sm"
|
|
>
|
|
+ Add User
|
|
</button>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr className="border-b border-gray-200 bg-white">
|
|
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Email / ID</th>
|
|
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Role</th>
|
|
<th className="py-3 px-6 text-gray-500 font-semibold text-xs uppercase tracking-wider">Joined Date</th>
|
|
<th className="py-3 px-6 text-right text-gray-500 font-semibold text-xs uppercase tracking-wider">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="text-sm">
|
|
{users.map((u, i) => (
|
|
<tr key={i} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
|
<td className="py-4 px-6 text-gray-900 font-medium">{u.email}</td>
|
|
<td className="py-4 px-6">
|
|
<span className={`px-2.5 py-1 rounded-md text-xs font-semibold ${u.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'}`}>
|
|
{u.role.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td className="py-4 px-6 text-gray-500">{new Date(u.createdAt).toLocaleDateString('en-US')}</td>
|
|
<td className="py-4 px-6 text-right">
|
|
<button
|
|
onClick={() => handleOpenLimits(u)}
|
|
className="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 hover:text-blue-600 rounded text-xs font-semibold transition-colors focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
Feature Limits
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'billing' && (
|
|
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-8">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Billing & Subscription</h2>
|
|
<p className="text-gray-500 text-sm mb-8">Choose the right plan to unlock Quantum XCU modules for your team.</p>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{packages.map(pkg => (
|
|
<div key={pkg.id} className="border border-gray-200 rounded-2xl p-6 hover:border-blue-500 transition-all group relative overflow-hidden">
|
|
<div className="font-bold text-lg text-gray-900 mb-1">{pkg.name}</div>
|
|
<div className="text-2xl font-black text-blue-600 mb-4">{pkg.price}</div>
|
|
<ul className="text-sm text-gray-500 space-y-2 mb-8">
|
|
{JSON.parse(pkg.features || '[]').slice(0, 4).map((f: string, idx: number) => (
|
|
<li key={idx} className="flex items-center gap-2">
|
|
<svg className="w-4 h-4 text-emerald-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd"></path></svg>
|
|
{f}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<button
|
|
disabled={checkoutLoading}
|
|
onClick={() => handleCheckout(pkg.id)}
|
|
className="w-full py-3 bg-blue-600 text-white rounded-xl font-bold hover:bg-blue-700 transition-all disabled:opacity-50"
|
|
>
|
|
{checkoutLoading ? "Connecting..." : "Upgrade Now"}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'security' && (
|
|
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-8">
|
|
<div className="flex items-center gap-4 mb-8">
|
|
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center text-white shadow-lg">
|
|
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd"></path></svg>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">Quantum Security Suite</h2>
|
|
<p className="text-gray-500 text-sm">Manage organizational cryptographic sovereignty and BYOK settings.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* TLS SOVEREIGN MODULE - ?? la Carte */}
|
|
{(() => {
|
|
let tlsState = 'HIDDEN';
|
|
try {
|
|
const lic = typeof tenant.licenses === 'string' ? JSON.parse(tenant.licenses || '{}') : (tenant.licenses || {});
|
|
tlsState = lic['tls_sovereign'] || 'HIDDEN';
|
|
} catch {}
|
|
if (tlsState === 'HIDDEN') return null;
|
|
return (
|
|
<div className={`mb-8 rounded-2xl border overflow-hidden ${
|
|
tlsState === 'GRANTED'
|
|
? 'bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 border-slate-700/50'
|
|
: 'bg-gradient-to-br from-amber-50 to-orange-50 border-amber-200'
|
|
}`}>
|
|
<div className="p-6">
|
|
<div className="flex items-center gap-4 mb-4">
|
|
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center shadow-lg ${
|
|
tlsState === 'GRANTED'
|
|
? 'bg-gradient-to-br from-cyan-500 to-blue-600 shadow-cyan-500/20'
|
|
: 'bg-gradient-to-br from-amber-400 to-orange-500 shadow-amber-500/20'
|
|
}`}>
|
|
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
</div>
|
|
<div>
|
|
<h3 className={`text-lg font-bold ${
|
|
tlsState === 'GRANTED' ? 'text-white' : 'text-amber-900'
|
|
}`}>TLS Sovereign Module</h3>
|
|
<p className={`text-xs ${
|
|
tlsState === 'GRANTED' ? 'text-slate-400' : 'text-amber-700'
|
|
}`}>?? la Carte ??? Sovereign Communication Infrastructure</p>
|
|
</div>
|
|
</div>
|
|
|
|
{tlsState === 'GRANTED' ? (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-2 ${
|
|
tenant.securityTier === 'SOVEREIGN'
|
|
? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
|
|
: tenant.securityTier === 'CLIENT_CA'
|
|
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
|
: 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
|
|
}`}>
|
|
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
{tenant.securityTier === 'SOVEREIGN' ? '\ud83d\udee1\ufe0f SOVEREIGN \u2014 Private CA (X)' : tenant.securityTier === 'CLIENT_CA' ? '\ud83c\udfdb\ufe0f CLIENT CA \u2014 Custom PKI' : '\ud83c\udf10 STANDARD \u2014 Let\'s Encrypt'}
|
|
</div>
|
|
<span className="relative flex h-2.5 w-2.5">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
|
</span>
|
|
<span className="text-[10px] text-emerald-400 font-bold uppercase">Active</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
|
<div className="text-[10px] text-slate-500 font-bold uppercase mb-1">Mode</div>
|
|
<div className="text-sm text-white font-bold">{tenant.securityTier === 'SOVEREIGN' ? 'Private CA (Offline-Ready)' : tenant.securityTier === 'CLIENT_CA' ? 'Client PKI (Custom CA)' : 'Let\'s Encrypt (Global)'}</div>
|
|
</div>
|
|
<div className="bg-white/5 rounded-xl p-3 border border-white/10">
|
|
<div className="text-[10px] text-slate-500 font-bold uppercase mb-1">Validity</div>
|
|
<div className="text-sm text-white font-bold">{tenant.securityTier === 'SOVEREIGN' ? '30 Years (RSA-4096)' : tenant.securityTier === 'CLIENT_CA' ? 'Per CA Policy' : 'Auto-Renew (90 Days)'}</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-[11px] text-slate-500 mt-2">
|
|
{tenant.securityTier === 'SOVEREIGN'
|
|
? 'Organisasi Anda menggunakan Private CA dari X. Hubungi Supreme Admin untuk panduan install sertifikat di device.'
|
|
: tenant.securityTier === 'CLIENT_CA'
|
|
? 'Organisasi Anda menggunakan CA sendiri (custom PKI). Sertifikat dan key di-manage oleh infrastruktur PKI internal organisasi.'
|
|
: 'Koneksi menggunakan sertifikat Let\'s Encrypt yang dipercaya semua browser. Untuk beralih ke Private CA, hubungi Supreme Admin.'
|
|
}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
/* UPSELL STATE */
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-amber-800 leading-relaxed">
|
|
Tingkatkan ke <strong>TLS Sovereign</strong> untuk mendapatkan infrastruktur komunikasi yang sepenuhnya tertutup
|
|
dengan Private CA, zero-trust security, dan kemampuan operasi offline tanpa bergantung internet global.
|
|
</p>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
|
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
<div className="text-[10px] font-bold text-amber-800">Private CA</div>
|
|
<div className="text-[9px] text-amber-600">RSA-4096</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
|
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 5.636a9 9 0 010 12.728M5.636 18.364a9 9 0 010-12.728" /></svg>
|
|
<div className="text-[10px] font-bold text-amber-800">Offline-Ready</div>
|
|
<div className="text-[9px] text-amber-600">Zero Internet</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl p-3 border border-amber-200 text-center">
|
|
<svg className="w-6 h-6 text-amber-600 mx-auto mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
|
<div className="text-[10px] font-bold text-amber-800">30 Years</div>
|
|
<div className="text-[9px] text-amber-600">Certificate Life</div>
|
|
</div>
|
|
</div>
|
|
<button className="w-full py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white rounded-xl font-bold text-sm hover:shadow-lg hover:shadow-amber-500/20 transition-all hover:scale-[1.01] flex items-center justify-center gap-2">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
|
Upgrade ke TLS Sovereign
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
<div className="bg-blue-50/50 border border-blue-100 rounded-2xl p-8">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
|
<div className="flex-1">
|
|
<h3 className="text-lg font-bold text-blue-900 mb-2 uppercase tracking-wide">Organizational BYOK</h3>
|
|
<p className="text-sm text-blue-700 leading-relaxed max-w-xl">
|
|
Aktifkan kedaulatan data total dengan menggunakan kunci enkripsi milik Anda sendiri.
|
|
Kunci ini akan digunakan untuk mengenkripsi semua transmisi video, audio, dan chat dalam organisasi Anda.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-col gap-4 w-full md:w-72">
|
|
<label className="flex items-center gap-3 cursor-pointer bg-white px-4 py-3 rounded-2xl border border-blue-200 shadow-sm transition-all hover:border-blue-400">
|
|
<input
|
|
type="checkbox"
|
|
checked={orgByokEnabled}
|
|
onChange={e => setOrgByokEnabled(e.target.checked)}
|
|
className="w-5 h-5 text-blue-600 rounded-lg focus:ring-blue-500"
|
|
/>
|
|
<span className="text-xs font-black text-gray-700 uppercase tracking-widest">Enable BYOK Mode</span>
|
|
</label>
|
|
{orgByokEnabled && (
|
|
<div className="space-y-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
<label className="text-[10px] font-black text-blue-600 uppercase ml-1">Custom Quantum Key</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Enter Encryption Key..."
|
|
value={orgByokKey}
|
|
onChange={e => setOrgByokKey(e.target.value)}
|
|
className="w-full text-xs font-mono bg-white border border-blue-200 rounded-2xl px-4 py-3 outline-none focus:ring-2 focus:ring-blue-500 shadow-sm"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 pt-8 border-t border-blue-100 flex justify-end">
|
|
<button
|
|
onClick={handleSaveOrgByok}
|
|
className="px-8 py-3 bg-blue-600 text-white rounded-2xl font-black text-xs uppercase tracking-[0.2em] hover:bg-blue-700 transition-all shadow-lg hover:shadow-blue-500/20 active:scale-95"
|
|
>
|
|
Deploy Security Policy
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-12">
|
|
<div className="flex items-center gap-2 text-emerald-600 mb-4">
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd"></path></svg>
|
|
<span className="text-xs font-black uppercase tracking-widest">Quantum Health Status: Protected</span>
|
|
</div>
|
|
<p className="text-[10px] text-gray-400 font-medium max-w-2xl italic">
|
|
Catatan: Setiap perubahan pada kebijakan keamanan akan dicatat dalam Quantum Ledger dan memerlukan waktu sinkronisasi hingga 50ms ke seluruh node global.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</main>
|
|
|
|
{/* ADD USER MODAL */}
|
|
{showAddUserModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-white p-8 rounded-xl shadow-2xl w-[400px]">
|
|
<h2 className="text-xl font-bold mb-6 text-gray-900">Add New User</h2>
|
|
<form onSubmit={handleAddUser}>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
|
|
<input type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500" required />
|
|
</div>
|
|
<div className="mb-6">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Temporary Password</label>
|
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-blue-500 focus:border-blue-500" required minLength={6} />
|
|
</div>
|
|
<div className="flex gap-3 justify-end">
|
|
<button type="button" onClick={() => setShowAddUserModal(false)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</button>
|
|
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Add User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* USER LIMITS MODAL (BYOK) */}
|
|
{showLimitsModal && selectedUser && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="bg-white w-full max-w-2xl rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
|
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
<h2 className="text-xl font-bold text-gray-900">User Feature Limits</h2>
|
|
<p className="text-sm text-gray-500 mt-1">Manage module access for <strong className="text-gray-800">{selectedUser.email}</strong>.</p>
|
|
</div>
|
|
|
|
<div className="p-6 overflow-y-auto flex-1">
|
|
{/* USER LEVEL BYOK SECTION */}
|
|
<div className="mb-10 bg-emerald-50/50 p-6 rounded-2xl border border-emerald-100">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
|
<div className="flex-1">
|
|
<h3 className="text-sm font-black text-emerald-900 uppercase tracking-widest mb-1">User BYOK Sovereignty</h3>
|
|
<p className="text-[10px] text-emerald-700 font-medium">Berikan kedaulatan enkripsi individual kepada user ini. Kunci ini akan melampaui kunci organisasi.</p>
|
|
</div>
|
|
<div className="flex flex-col gap-2 w-full md:w-64">
|
|
<label className="flex items-center gap-2 cursor-pointer bg-white px-3 py-2 rounded-xl border border-emerald-200">
|
|
<input type="checkbox" checked={userByokEnabled} onChange={e => setUserByokEnabled(e.target.checked)} className="w-4 h-4 text-emerald-600 rounded" />
|
|
<span className="text-[10px] font-bold text-gray-700">Enable Individual BYOK</span>
|
|
</label>
|
|
{userByokEnabled && (
|
|
<input
|
|
type="text"
|
|
placeholder="Individual Quantum Key..."
|
|
value={userByokKey}
|
|
onChange={e => setUserByokKey(e.target.value)}
|
|
className="text-[10px] font-mono bg-white border border-emerald-200 rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-emerald-500"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{(() => {
|
|
// Get tenant's granted features
|
|
let tenantGranted: Record<string, string> = {};
|
|
try {
|
|
const raw = tenant?.licenses;
|
|
if (typeof raw === 'string') {
|
|
const js = JSON.parse(raw);
|
|
if (Array.isArray(js)) js.forEach((k: string) => tenantGranted[k] = "GRANTED");
|
|
else tenantGranted = js;
|
|
} else if (raw && typeof raw === 'object') {
|
|
if (Array.isArray(raw)) raw.forEach((k: string) => tenantGranted[k] = "GRANTED");
|
|
else tenantGranted = raw;
|
|
}
|
|
} catch(_e) {}
|
|
|
|
// Render features by category
|
|
return ['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
|
const filtered = systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM'));
|
|
if (filtered.length === 0) return null;
|
|
|
|
return (
|
|
<div key={mod} className="mb-8 last:mb-0">
|
|
<div className="text-[10px] font-black text-gray-400 mb-3 tracking-[0.2em] border-l-4 border-blue-500 pl-3 uppercase">{mod} CAPABILITIES</div>
|
|
<div className="space-y-3">
|
|
{filtered.map((feat: Feature) => {
|
|
const tStatus = tenantGranted[feat.key] || 'HIDDEN';
|
|
|
|
if (tStatus === 'HIDDEN') return null; // Invisible to tenant completely
|
|
|
|
if (tStatus === 'UPSELL') {
|
|
return (
|
|
<div key={feat.key} className="flex justify-between items-center bg-gray-50 p-4 rounded-lg border border-gray-200 opacity-70">
|
|
<div>
|
|
<div className="font-bold text-gray-600 flex items-center gap-2 text-sm">
|
|
<svg className="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd"></path></svg>
|
|
{feat.name}
|
|
</div>
|
|
<div className="text-[10px] text-gray-500 font-mono mt-0.5">{feat.key}</div>
|
|
</div>
|
|
<div
|
|
onClick={() => setActiveTab('billing')}
|
|
className="text-[10px] font-bold text-blue-600 px-3 py-1 bg-blue-50 border border-blue-200 rounded cursor-pointer hover:bg-blue-100 transition-colors uppercase tracking-wider"
|
|
>
|
|
Upgrade Plan
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// If GRANTED by Superadmin
|
|
const currentVal = userLicenses[feat.key] || 'GRANTED';
|
|
return (
|
|
<div key={feat.key} className="flex justify-between items-center bg-white p-4 rounded-lg border border-gray-200">
|
|
<div>
|
|
<div className="font-bold text-gray-900 text-sm">{feat.name}</div>
|
|
<div className="text-[10px] text-gray-500 font-mono mt-0.5">{feat.key}</div>
|
|
</div>
|
|
<select
|
|
value={currentVal}
|
|
onChange={(e) => setUserLicenses({...userLicenses, [feat.key]: e.target.value})}
|
|
className={`text-[10px] font-bold px-3 py-1.5 rounded outline-none border focus:ring-2 focus:ring-blue-500 ${
|
|
currentVal === 'GRANTED' ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
|
|
}`}
|
|
>
|
|
<option value="GRANTED">✅ ALLOWED</option>
|
|
<option value="HIDDEN">🚫 BLOCKED</option>
|
|
</select>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
})()}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-3">
|
|
<button onClick={() => setShowLimitsModal(false)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">Cancel</button>
|
|
<button onClick={handleSaveLimits} className="px-6 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700">Save Limits</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|