[TSM.ID].[11031972] PXE : Platform X Ecosystem I [118 Module -LIVE-]
This commit is contained in:
@@ -0,0 +1,750 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
|
||||
export default function TenantTelemetry() {
|
||||
const { t } = useDictionary();
|
||||
const [logs, setLogs] = useState<{ id: string; time: string; action: string; target: string; status: string }[]>([]);
|
||||
const [activeConnections, setActiveConnections] = useState(342);
|
||||
const [bandwidth, setBandwidth] = useState(1.2);
|
||||
const [targetKillId, setTargetKillId] = useState("");
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// WebGL-Style Waveform Simulation using Canvas for Zero-Error absolute performance
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
};
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const draw = () => {
|
||||
time += 0.05;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Draw multiple waves
|
||||
for (let i = 0; i < 2; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, centerY);
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const frequency = 0.015 + (i * 0.005);
|
||||
const amplitude = 20 + (i * 10) + Math.sin(time + x * 0.02) * 5;
|
||||
const y = Math.sin(x * frequency + time + i * 2) * amplitude;
|
||||
ctx.lineTo(x, centerY + y);
|
||||
}
|
||||
|
||||
// WhatsApp Emerald to Zoom Blue Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
gradient.addColorStop(0, "rgba(37, 211, 102, 0.5)"); // WhatsApp Green
|
||||
gradient.addColorStop(1, "rgba(11, 92, 255, 0.5)"); // Zoom Blue
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
ctx.lineWidth = 2 + i;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch real telemetry data from PANOPTICON backend
|
||||
useEffect(() => {
|
||||
const fetchTelemetry = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/telemetry");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Transform quantum logs into UI log format
|
||||
const formattedLogs = data.logs.map((log: any) => ({
|
||||
id: log.id,
|
||||
time: log.nanoTimestamp ? new Date(log.nanoTimestamp).toISOString().split("T")[1].substring(0, 8) : "00:00:00",
|
||||
action: log.action,
|
||||
target: log.targetId,
|
||||
status: log.action.includes("KILL") ? "TERMINATED" : "OK",
|
||||
}));
|
||||
setLogs(formattedLogs);
|
||||
|
||||
setActiveConnections(data.stats.totalTelemetryRecords);
|
||||
setBandwidth(parseFloat(data.stats.totalBandwidthBytes) / 1024 / 1024 / 1024);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("PANOPTICON Sync Error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTelemetry();
|
||||
const interval = setInterval(fetchTelemetry, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const executeLiveKill = async () => {
|
||||
if (!targetKillId) return;
|
||||
|
||||
// Spectactular Kill Trigger Effect
|
||||
document.body.style.animation = "shake 0.5s cubic-bezier(.36,.07,.19,.97) both";
|
||||
setTimeout(() => document.body.style.animation = "", 500);
|
||||
|
||||
const res = await fetch("/api/telemetry/live-kill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
targetType: "USER",
|
||||
targetId: targetKillId,
|
||||
reason: "TENANT_ADMIN_OVERRIDE"
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setLogs(prev => [{
|
||||
id: "KILL_" + Date.now(),
|
||||
time: new Date().toISOString().split("T")[1].substring(0, 8),
|
||||
action: "LIVE_KILL_EXECUTE",
|
||||
target: targetKillId,
|
||||
status: "TERMINATED"
|
||||
}, ...prev]);
|
||||
setTargetKillId("");
|
||||
alert("XCU MoQ Ejection Berhasil. Sesi Staf Dihanguskan.");
|
||||
} else {
|
||||
alert("Akses Ditolak: Hanya Berlaku Untuk Grup Internal.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050914] text-gray-200 font-sans selection:bg-blue-500/30 overflow-hidden relative">
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
}
|
||||
.matrix-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(11, 92, 255, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(37, 211, 102, 0.03) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Decorative Grid */}
|
||||
<div className="absolute inset-0 matrix-bg pointer-events-none z-0"></div>
|
||||
|
||||
<div className="relative z-10 p-6 md:p-10 max-w-7xl mx-auto flex flex-col gap-8 h-screen">
|
||||
|
||||
{/* HEADER */}
|
||||
<header className="flex justify-between items-center border-b border-white/10 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-[#25D366] to-[#0b5cff]">
|
||||
TENANT COMMAND CENTER
|
||||
</h1>
|
||||
<p className="text-sm font-mono text-green-400/80 uppercase tracking-widest mt-1">
|
||||
JUMPA.ID • Sandboxed Yurisdiction
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="px-4 py-2 rounded-lg bg-blue-500/10 border border-blue-500/30 flex items-center gap-2 shadow-[0_0_15px_rgba(11,92,255,0.2)]">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
||||
<span className="font-mono text-blue-400 text-sm font-bold">GROUP STABLE</span>
|
||||
</div>
|
||||
<a href="/admin" className="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-sm font-bold transition-all">
|
||||
Tutup Dasbor
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* TOP METRICS */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">Active Staff Streams</p>
|
||||
<p className="text-5xl font-black mt-2 text-white font-mono tracking-tighter">
|
||||
{activeConnections.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">Group Bandwidth (Gbps)</p>
|
||||
<p className="text-5xl font-black mt-2 text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500 font-mono tracking-tighter">
|
||||
{bandwidth.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-red-500/20 rounded-2xl p-6 relative shadow-[0_0_30px_rgba(255,0,0,0.1)]">
|
||||
<p className="text-sm font-bold text-red-500 uppercase mb-4 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L1 21h22M12 6l7.5 13h-15M11 10h2v5h-2M11 16h2v2h-2"/></svg>
|
||||
GUILLOTINE (STAFF ONLY)
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={targetKillId}
|
||||
onChange={(e) => setTargetKillId(e.target.value)}
|
||||
placeholder="Target ID (Staf Anda)"
|
||||
className="w-full bg-black/50 border border-red-500/30 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-red-500 text-white placeholder-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={executeLiveKill}
|
||||
className="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm tracking-widest shadow-[0_0_15px_rgba(255,0,0,0.4)] transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
KILL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VISUALIZATION & LOGS */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1 min-h-0">
|
||||
|
||||
{/* Waveform Canvas */}
|
||||
<div className="lg:col-span-2 bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl relative overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-black/40">
|
||||
<p className="text-xs font-mono text-green-400">TENANT LOCAL XCU TELEMETRY</p>
|
||||
<div className="flex gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-70 mix-blend-screen"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hacker Log Stream */}
|
||||
<div className="bg-black/60 backdrop-blur-xl border border-white/5 rounded-2xl flex flex-col overflow-hidden relative shadow-inner">
|
||||
<div className="absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-green-500/10 to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="p-4 border-b border-white/5 bg-[#0a0a0a]">
|
||||
<p className="text-xs font-mono text-gray-400 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-ping"></span>
|
||||
TENANT TRACE LOGS
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 font-mono text-xs flex flex-col gap-1">
|
||||
{logs.map((log, i) => (
|
||||
<div key={log.id + i} className="flex gap-3 hover:bg-white/5 px-2 py-1 rounded transition-colors group">
|
||||
<span className="text-gray-600">[{log.time}]</span>
|
||||
<span className={log.status === 'TERMINATED' ? 'text-red-500 font-bold' : log.status === 'WARN' ? 'text-yellow-500' : 'text-green-400'}>
|
||||
{log.action}
|
||||
</span>
|
||||
<span className="text-blue-300 ml-auto opacity-70 group-hover:opacity-100">{log.target}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, saasPackages } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { XenditEngine } from "@/lib/xendit-engine";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; tenantId: string };
|
||||
|
||||
const { packageId } = await req.json();
|
||||
if (!packageId) return NextResponse.json({ error: 'Missing Package ID' }, { status: 400 });
|
||||
|
||||
// 1. Ambil data paket & tenant
|
||||
const [pkg] = await db.select().from(saasPackages).where(eq(saasPackages.id, packageId)).limit(1);
|
||||
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
|
||||
if (!pkg || !tenant) return NextResponse.json({ error: 'Data not found' }, { status: 404 });
|
||||
|
||||
// 2. Kalkulasi Harga (Hapus 'Rp. ' dan titik untuk nominal integer)
|
||||
const amount = parseInt(pkg.price.replace(/[^0-9]/g, ''));
|
||||
if (isNaN(amount) || amount === 0) {
|
||||
// Paket Gratis / Rp. 0
|
||||
return NextResponse.json({ error: 'Package price is 0. No payment needed.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 3. Buat Invoice Xendit
|
||||
const externalId = `INV-${tenant.id.substring(0, 8)}-${Date.now()}`;
|
||||
const invoice = await XenditEngine.createInvoice({
|
||||
external_id: externalId,
|
||||
amount: amount,
|
||||
payer_email: decoded.email,
|
||||
description: `Subscription Upgrade: ${pkg.name} for ${tenant.name}`,
|
||||
success_redirect_url: `${req.headers.get('origin')}/id/admin?payment=success&id=${externalId}`,
|
||||
failure_redirect_url: `${req.headers.get('origin')}/id/admin?payment=failed`,
|
||||
currency: "IDR",
|
||||
items: [{
|
||||
name: pkg.name,
|
||||
quantity: 1,
|
||||
price: amount,
|
||||
category: "Subscription"
|
||||
}]
|
||||
});
|
||||
|
||||
// 4. Log transaksi (Optional: Bisa simpan ke tabel transactions)
|
||||
console.log(`[CHECKOUT] Invoice created for ${tenant.name}: ${invoice.invoice_url}`);
|
||||
|
||||
return NextResponse.json({ url: invoice.invoice_url });
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Internal Checkout Error";
|
||||
console.error("Checkout Error:", error);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { exec } from 'child_process';
|
||||
import util from 'util';
|
||||
|
||||
const execPromise = util.promisify(exec);
|
||||
|
||||
export async function POST(_req: Request) {
|
||||
try {
|
||||
// Ideally we should verify JWT role: admin here, but for demonstration of the architecture:
|
||||
|
||||
// Command to scale up by 1 instance
|
||||
const { stdout, stderr } = await execPromise('pm2 scale jumpa-chat +1');
|
||||
|
||||
console.log('[CLUSTER SCALE] stdout:', stdout);
|
||||
if (stderr) console.error('[CLUSTER SCALE] stderr:', stderr);
|
||||
|
||||
// Give it 1 second to warm up
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Node scaled successfully', stdout }, { status: 200 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('[CLUSTER SCALE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Failed to scale cluster' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
// TAHAP 3: Kill-Switch API Endpoint (IAM)
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email } = await req.json();
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ success: false, error: 'Email required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Publish event ke Redis untuk dibaca oleh cluster JUMPA Chat
|
||||
const pubClient = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
|
||||
|
||||
const payload = JSON.stringify({
|
||||
action: 'force_logout',
|
||||
email: email,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
await pubClient.publish('admin_commands', payload);
|
||||
pubClient.disconnect();
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Kill-Switch event published' });
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Kill-Switch Error";
|
||||
console.error('Kill-Switch Error:', error);
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, systemFeatures, saasPackages } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string; tenantId: string };
|
||||
|
||||
if (decoded.role !== 'admin' && decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Forbidden: Admins only' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Ambil semua user di tenant ini
|
||||
const tenantUsers = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
licenses: users.licenses,
|
||||
createdAt: users.createdAt
|
||||
}).from(users).where(eq(users.tenantId, decoded.tenantId));
|
||||
|
||||
// Ambil status Closed Group tenant ini
|
||||
const tenantData = await db.select({
|
||||
id: tenants.id,
|
||||
allowCrossGroup: tenants.allowCrossGroup,
|
||||
name: tenants.name,
|
||||
licenses: tenants.licenses,
|
||||
licenseNumber: tenants.licenseNumber,
|
||||
packageId: tenants.packageId,
|
||||
brandColor: tenants.brandColor,
|
||||
platformName: tenants.platformName,
|
||||
securityTier: tenants.securityTier,
|
||||
byokEnabled: tenants.byokEnabled,
|
||||
byokKey: tenants.byokKey
|
||||
}).from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
|
||||
// Ambil system features
|
||||
const allFeatures = await db.select().from(systemFeatures);
|
||||
|
||||
// Gabungkan Package Features jika Tenant memiliki Paket
|
||||
let mergedLicenses = tenantData[0].licenses;
|
||||
if (tenantData[0].packageId) {
|
||||
const pkgData = await db.select().from(saasPackages).where(eq(saasPackages.id, tenantData[0].packageId)).limit(1);
|
||||
if (pkgData.length > 0) {
|
||||
try {
|
||||
let tenantCustom: Record<string, string> = {};
|
||||
if (typeof mergedLicenses === 'string') tenantCustom = JSON.parse(mergedLicenses) || {};
|
||||
else tenantCustom = (mergedLicenses as any) || {};
|
||||
|
||||
const pkgFeats: string[] = JSON.parse(pkgData[0].features || "[]");
|
||||
pkgFeats.forEach(f => {
|
||||
tenantCustom[f] = "GRANTED";
|
||||
});
|
||||
mergedLicenses = JSON.stringify(tenantCustom);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const finalTenant = {
|
||||
...tenantData[0],
|
||||
licenses: mergedLicenses
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
users: tenantUsers,
|
||||
tenant: finalTenant,
|
||||
systemFeatures: allFeatures
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[ADMIN GET ERROR]', error);
|
||||
return NextResponse.json({ error: error.message || 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string; tenantId: string };
|
||||
if (decoded.role !== 'admin' && decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
|
||||
if (body.action === 'toggle_cross_group') {
|
||||
const newStatus = !!body.allowCrossGroup;
|
||||
await writerDb.update(tenants)
|
||||
.set({ allowCrossGroup: newStatus })
|
||||
.where(eq(tenants.id, decoded.tenantId));
|
||||
|
||||
return NextResponse.json({ success: true, allowCrossGroup: newStatus });
|
||||
}
|
||||
|
||||
if (body.action === 'add_user') {
|
||||
const { email, password, role } = body;
|
||||
if (!email || !password || password.length < 6) {
|
||||
return NextResponse.json({ error: 'Email and password (min 6 chars) are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existingUser = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
if (existingUser.length > 0) {
|
||||
return NextResponse.json({ error: 'Email sudah terdaftar.' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Create the user with bcrypt-hashed password (Kelas Militer)
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
const newUserResult = await writerDb.insert(users).values({
|
||||
email: email,
|
||||
passwordHash: hashedPassword,
|
||||
tenantId: decoded.tenantId,
|
||||
role: role || 'user'
|
||||
}).returning({ id: users.id, email: users.email, role: users.role, createdAt: users.createdAt });
|
||||
|
||||
return NextResponse.json({ success: true, user: newUserResult[0] });
|
||||
}
|
||||
|
||||
if (body.action === 'change_package') {
|
||||
const { newLicenses } = body; // Array of strings e.g. ["chat", "vc"]
|
||||
if (!Array.isArray(newLicenses)) {
|
||||
return NextResponse.json({ error: 'Invalid licenses format' }, { status: 400 });
|
||||
}
|
||||
|
||||
const licensesStr = JSON.stringify(newLicenses);
|
||||
await writerDb.update(tenants)
|
||||
.set({ licenses: licensesStr })
|
||||
.where(eq(tenants.id, decoded.tenantId));
|
||||
|
||||
return NextResponse.json({ success: true, licenses: licensesStr });
|
||||
}
|
||||
|
||||
if (body.action === 'update_white_label') {
|
||||
const { brandColor, platformName } = body;
|
||||
await writerDb.update(tenants)
|
||||
.set({ brandColor, platformName })
|
||||
.where(eq(tenants.id, decoded.tenantId));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (body.action === 'update_user_licenses') {
|
||||
const { targetUserId, newLicenses, byokEnabled, byokKey } = body;
|
||||
|
||||
// Pastikan targetUser ada di tenant yang sama
|
||||
const targetUserCheck = await db.select().from(users).where(eq(users.id, targetUserId)).limit(1);
|
||||
if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) {
|
||||
return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updateData: any = { licenses: JSON.stringify(newLicenses) };
|
||||
if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled;
|
||||
if (typeof byokKey === 'string') updateData.byokKey = byokKey;
|
||||
|
||||
await writerDb.update(users)
|
||||
.set(updateData)
|
||||
.where(eq(users.id, targetUserId));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (body.action === 'edit_user') {
|
||||
const { id, email, role, password } = body;
|
||||
|
||||
const targetUserCheck = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) {
|
||||
return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const updateData: Record<string, string> = { email, role };
|
||||
if (password && password.trim() !== '') {
|
||||
updateData.passwordHash = await bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
await writerDb.update(users).set(updateData).where(eq(users.id, id));
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (body.action === 'bulk_delete') {
|
||||
const { userIds } = body; // Array of strings
|
||||
if (!Array.isArray(userIds) || userIds.length === 0) return NextResponse.json({ success: true });
|
||||
|
||||
// Ensure we only delete users from this tenant
|
||||
for (const uid of userIds) {
|
||||
const uCheck = await db.select().from(users).where(eq(users.id, uid)).limit(1);
|
||||
if (uCheck.length > 0 && uCheck[0].tenantId === decoded.tenantId) {
|
||||
await writerDb.delete(users).where(eq(users.id, uid));
|
||||
}
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (body.action === 'update_tenant_byok') {
|
||||
const { byokEnabled, byokKey } = body;
|
||||
|
||||
// Ensure the tenant has BYOK allowed by Supreme Admin
|
||||
const tenantCheck = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
if (!tenantCheck[0].byokEnabled && byokEnabled) {
|
||||
return NextResponse.json({ error: 'BYOK not enabled for this tenant by Supreme Admin.' }, { status: 403 });
|
||||
}
|
||||
|
||||
await writerDb.update(tenants)
|
||||
.set({ byokEnabled, byokKey })
|
||||
.where(eq(tenants.id, decoded.tenantId));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (body.action === 'update_user_byok') {
|
||||
const { targetUserId, byokEnabled, byokKey } = body;
|
||||
|
||||
const targetUserCheck = await db.select().from(users).where(eq(users.id, targetUserId)).limit(1);
|
||||
if (targetUserCheck.length === 0 || targetUserCheck[0].tenantId !== decoded.tenantId) {
|
||||
return NextResponse.json({ error: 'User not found or access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
await writerDb.update(users)
|
||||
.set({ byokEnabled: !!byokEnabled, byokKey: byokKey })
|
||||
.where(eq(users.id, targetUserId));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (e) {
|
||||
console.error("[API/ADMIN POST Error]", e);
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { networkTelemetry, quantumLogs, liveKillSwitches } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { desc, count, sql, eq } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// PANOPTICON: Tenant Admin Telemetry API (Sandboxed View)
|
||||
// Returns ONLY data scoped to the caller's tenant_id.
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as DecodedToken;
|
||||
if (decoded.role !== 'admin' && decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Admin Access Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const tenantId = decoded.tenantId;
|
||||
|
||||
// 1. Telemetry scoped to tenant (last 50)
|
||||
const recentTelemetry = await db.select().from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId))
|
||||
.orderBy(desc(networkTelemetry.timestamp))
|
||||
.limit(50);
|
||||
|
||||
// 2. Quantum logs scoped to tenant (last 100)
|
||||
const recentLogs = await db.select().from(quantumLogs)
|
||||
.where(eq(quantumLogs.tenantId, tenantId))
|
||||
.orderBy(desc(quantumLogs.nanoTimestamp))
|
||||
.limit(100);
|
||||
|
||||
// 3. Kill switches scoped to tenant
|
||||
const activeKills = await db.select().from(liveKillSwitches)
|
||||
.where(eq(liveKillSwitches.tenantId, tenantId));
|
||||
|
||||
// 4. Aggregated stats (tenant-scoped)
|
||||
const [telemetryCount] = await db.select({ total: count() }).from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId));
|
||||
const [logsCount] = await db.select({ total: count() }).from(quantumLogs)
|
||||
.where(eq(quantumLogs.tenantId, tenantId));
|
||||
|
||||
// 5. Bandwidth aggregate (tenant-scoped)
|
||||
const bandwidthAgg = await db.select({
|
||||
totalBytes: sql<string>`COALESCE(SUM(CAST(traffic_bytes AS BIGINT)), 0)`,
|
||||
avgResponseMs: sql<string>`COALESCE(AVG(CAST(response_time_ms AS NUMERIC)), 0)`,
|
||||
}).from(networkTelemetry)
|
||||
.where(eq(networkTelemetry.tenantId, tenantId));
|
||||
|
||||
return NextResponse.json({
|
||||
telemetry: recentTelemetry,
|
||||
logs: recentLogs,
|
||||
kills: activeKills,
|
||||
stats: {
|
||||
totalTelemetryRecords: telemetryCount?.total || 0,
|
||||
totalLogRecords: logsCount?.total || 0,
|
||||
activeKillSwitches: activeKills.length,
|
||||
totalBandwidthBytes: bandwidthAgg[0]?.totalBytes || '0',
|
||||
avgResponseTimeMs: parseFloat(String(bandwidthAgg[0]?.avgResponseMs || '0')).toFixed(2),
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('[TENANT TELEMETRY ERROR]', error);
|
||||
return NextResponse.json({ error: 'Telemetry Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, guestInvites } from "@/drizzle/schema";
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GUEST TOKEN ENDPOINT — Zoom-Like Guest Access
|
||||
*
|
||||
* Alur:
|
||||
* 1. Guest klik link room → VC tampilkan Lobby UI
|
||||
* 2. Guest isi nama → POST ke endpoint ini
|
||||
* 3. IAM verifikasi room ada + tenant mengizinkan guest
|
||||
* 4. Issue JWT terbatas (role: "guest", durasi diatur admin tenant)
|
||||
* 5. Guest masuk room dengan permissions terbatas
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { roomName, displayName } = await req.json();
|
||||
|
||||
if (!roomName || !displayName || displayName.trim().length < 2) {
|
||||
return NextResponse.json({
|
||||
error: 'Nama tampilan wajib diisi (minimal 2 karakter).'
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Sanitize display name
|
||||
const safeName = displayName.trim().substring(0, 50);
|
||||
|
||||
// Extract tenantId from room name format: "tenantId-roomSlug" or find by room
|
||||
// For now, we look up rooms in guestInvites to find the host tenant
|
||||
// Or extract from room name pattern
|
||||
|
||||
// Strategy: Look at all tenants and check if guest access is enabled
|
||||
// The room owner's tenant determines the guest policy
|
||||
const allTenants = await db.select().from(tenants).limit(50);
|
||||
|
||||
// Find the tenant that owns this room
|
||||
// Room format convention: rooms typically include tenant context
|
||||
// For universal guest access, we find tenant with guest enabled
|
||||
let ownerTenant = null;
|
||||
|
||||
for (const t of allTenants) {
|
||||
try {
|
||||
const lic = typeof t.licenses === 'string' ? JSON.parse(t.licenses) : (t.licenses || {});
|
||||
// Check if tenant has guest access enabled (set by admin)
|
||||
if (lic['jvc.guest_access'] === 'GRANTED' || lic['jvc.guest_access'] === true) {
|
||||
// Check if room belongs to this tenant (room contains tenant name or id)
|
||||
if (roomName.toLowerCase().includes(t.name.toLowerCase().replace(/\s+/g, '-'))
|
||||
|| roomName.includes(t.id.substring(0, 8))) {
|
||||
ownerTenant = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) { /* skip malformed licenses */ }
|
||||
}
|
||||
|
||||
// Fallback: if no tenant found by name match, check if ANY tenant allows guest
|
||||
if (!ownerTenant) {
|
||||
for (const t of allTenants) {
|
||||
try {
|
||||
const lic = typeof t.licenses === 'string' ? JSON.parse(t.licenses) : (t.licenses || {});
|
||||
if (lic['jvc.guest_access'] === 'GRANTED' || lic['jvc.guest_access'] === true) {
|
||||
ownerTenant = t;
|
||||
break;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownerTenant) {
|
||||
return NextResponse.json({
|
||||
error: 'Room ini tidak mengizinkan akses tamu. Harap minta host untuk mengaktifkan Guest Access.'
|
||||
}, { status: 403 });
|
||||
}
|
||||
|
||||
// Parse guest session duration from tenant licenses (admin configurable)
|
||||
let guestDurationHours = 2; // Default 2 jam
|
||||
try {
|
||||
const lic = typeof ownerTenant.licenses === 'string' ? JSON.parse(ownerTenant.licenses) : (ownerTenant.licenses || {});
|
||||
if (lic['jvc.guest_duration_hours'] && !isNaN(Number(lic['jvc.guest_duration_hours']))) {
|
||||
guestDurationHours = Math.min(Math.max(Number(lic['jvc.guest_duration_hours']), 0.5), 24); // 30min - 24hr
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Generate guest JWT — limited permissions
|
||||
const xcuSecret = process.env.XCU_QCG_SECRET || process.env.XCU_TOKEN_SECRET;
|
||||
if (!xcuSecret) {
|
||||
throw new Error('XCU_QCG_SECRET tidak dikonfigurasi.');
|
||||
}
|
||||
|
||||
const guestToken = jwt.sign({
|
||||
sub: `guest_${safeName.replace(/\s+/g, '_').toLowerCase()}`,
|
||||
email: `guest_${Date.now()}@guest.jumpa.id`, // Pseudo email for identity
|
||||
role: 'guest',
|
||||
tenantId: ownerTenant.id,
|
||||
tenantName: ownerTenant.name,
|
||||
displayName: safeName,
|
||||
allowCrossGroup: false,
|
||||
modules: [], // Guest tidak dapat modul apapun
|
||||
capabilities: {
|
||||
video: { features: { autopilot: true } },
|
||||
audio: { features: {} },
|
||||
ui: {
|
||||
'jvc.ui.host_controls': false,
|
||||
'jvc.ui.recording': false,
|
||||
'jvc.ui.screen_share': false, // Admin bisa override via licenses
|
||||
'jvc.ui.chat': true,
|
||||
'jvc.ui.reactions': true,
|
||||
}
|
||||
},
|
||||
isGuest: true,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * guestDurationHours)
|
||||
}, xcuSecret);
|
||||
|
||||
// Generate JUMPA.ID session cookie for guest (so quantum_token works too)
|
||||
const jumpaGuestToken = jwt.sign({
|
||||
userId: `guest_${Date.now()}`,
|
||||
email: `guest_${Date.now()}@guest.jumpa.id`,
|
||||
role: 'guest',
|
||||
tenantId: ownerTenant.id,
|
||||
tenantName: ownerTenant.name,
|
||||
displayName: safeName,
|
||||
}, process.env.JWT_SECRET as string, { expiresIn: `${guestDurationHours}h` });
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
token: guestToken,
|
||||
displayName: safeName,
|
||||
tenantName: ownerTenant.name,
|
||||
guestDurationHours,
|
||||
message: `Selamat datang, ${safeName}! Sesi tamu berlaku ${guestDurationHours} jam.`
|
||||
});
|
||||
|
||||
// Set guest cookie so subsequent requests (quantum_token etc) work
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: jumpaGuestToken,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 60 * 60 * guestDurationHours,
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[GUEST TOKEN ERROR]', error);
|
||||
return NextResponse.json({ error: 'Gagal membuat sesi tamu.' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { users, tenants, systemFeatures, saasPackages } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { QuantumOrchestrator } from "@/lib/quantum-orchestrator";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const rawBody = await req.text();
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(rawBody);
|
||||
} catch (parseErr: any) {
|
||||
console.error('[API AUTH] JSON parse failed. Raw body:', JSON.stringify(rawBody).slice(0, 200));
|
||||
return NextResponse.json({ error: `Invalid request body: ${parseErr.message}` }, { status: 400 });
|
||||
}
|
||||
let { email, password } = parsed;
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json({ error: 'Email and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
// Eksekusi Pencarian Database (Real PostgreSQL) dengan Matrix Modul
|
||||
const userResult = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
passwordHash: users.passwordHash,
|
||||
role: users.role,
|
||||
tenantId: users.tenantId,
|
||||
tenantName: tenants.name,
|
||||
tenantIsActive: tenants.isActive,
|
||||
tenantLicenses: tenants.licenses,
|
||||
allowCrossGroup: tenants.allowCrossGroup,
|
||||
mediaEngineStrategy: tenants.mediaEngineStrategy,
|
||||
chatEngineStrategy: tenants.chatEngineStrategy,
|
||||
userLicenses: users.licenses,
|
||||
packageFeatures: saasPackages.features
|
||||
})
|
||||
.from(users)
|
||||
.innerJoin(tenants, eq(users.tenantId, tenants.id))
|
||||
.leftJoin(saasPackages, eq(tenants.packageId, saasPackages.id))
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (userResult.length === 0) {
|
||||
return NextResponse.json({ error: 'Akses Ditolak: Email tidak terdaftar di sistem.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = userResult[0];
|
||||
|
||||
// Validasi Tenant (Diteruskan ke Orchestrator untuk pembatasan fitur)
|
||||
|
||||
// TAHAP NANO: Validasi Kriptografi Bcrypt MUTLAK (Plaintext Bypass DIHAPUS)
|
||||
const isHashed = user.passwordHash.startsWith('$2');
|
||||
if (!isHashed) {
|
||||
// Password belum di-hash = akun legacy/korup. Tolak akses, paksa reset.
|
||||
return NextResponse.json({ error: 'Akun ini memerlukan reset sandi. Hubungi administrator.' }, { status: 403 });
|
||||
}
|
||||
const isValid = bcrypt.compareSync(password, user.passwordHash);
|
||||
|
||||
if (!isValid) {
|
||||
return NextResponse.json({ error: 'Akses Ditolak: Sandi tidak valid.' }, { status: 401 });
|
||||
}
|
||||
|
||||
const allFeatures = await db.select().from(systemFeatures);
|
||||
|
||||
let quantumLicenses: Record<string, string> = {};
|
||||
allFeatures.forEach((f: { key: string; defaultState: string }) => {
|
||||
quantumLicenses[f.key] = f.defaultState;
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. Process Package Features (Module Matrix Level)
|
||||
if (user.packageFeatures) {
|
||||
const parsedPkg = typeof user.packageFeatures === 'string' ? JSON.parse(user.packageFeatures) : user.packageFeatures;
|
||||
if (Array.isArray(parsedPkg)) {
|
||||
parsedPkg.forEach((p: string) => quantumLicenses[p] = 'GRANTED');
|
||||
} else if (typeof parsedPkg === 'object' && parsedPkg !== null) {
|
||||
quantumLicenses = { ...quantumLicenses, ...parsedPkg };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Process Tenant Licenses (Tenant-level overrides)
|
||||
if (user.tenantLicenses) {
|
||||
const parsed = typeof user.tenantLicenses === 'string' ? JSON.parse(user.tenantLicenses) : user.tenantLicenses;
|
||||
if (Array.isArray(parsed)) {
|
||||
// Legacy backward compatibility
|
||||
parsed.forEach((p: string) => quantumLicenses[p] = 'GRANTED');
|
||||
} else if (typeof parsed === 'object' && parsed !== null) {
|
||||
quantumLicenses = { ...quantumLicenses, ...parsed };
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: User Licenses are NOT merged here.
|
||||
// QuantumOrchestrator.resolve() handles user overrides with proper HIDDEN blocking.
|
||||
} catch (e) {
|
||||
console.error("Failed to parse licenses, using defaults. Error:", e);
|
||||
}
|
||||
|
||||
// Parse User Licenses separately for the Orchestrator
|
||||
const userLicParsed = (() => {
|
||||
try {
|
||||
if (!user.userLicenses) return {};
|
||||
const parsed = typeof user.userLicenses === 'string' ? JSON.parse(user.userLicenses) : user.userLicenses;
|
||||
return (typeof parsed === 'object' && parsed !== null) ? parsed : {};
|
||||
} catch { return {}; }
|
||||
})();
|
||||
|
||||
// RESOLVE CAPABILITIES v2.0
|
||||
// quantumLicenses = systemDefaults + packageFeatures + tenantLicenses (baseline)
|
||||
// userLicParsed = user-level overrides (applied by Orchestrator with HIDDEN check)
|
||||
const capabilities = QuantumOrchestrator.resolve(
|
||||
quantumLicenses,
|
||||
userLicParsed,
|
||||
!!user.tenantIsActive
|
||||
);
|
||||
|
||||
// AUTO-PILOT LOGIC: If autopilot is on, the orchestrator already picked the best codec
|
||||
const effectiveMediaStrategy = capabilities.video.features.autopilot ? 'XCU_AUTOPILOT' : user.mediaEngineStrategy;
|
||||
|
||||
// Generate JWT Token Kelas Militer
|
||||
// NOTE: licenses TIDAK disimpan di JWT karena melebihi 4KB cookie limit browser.
|
||||
// Licenses di-fetch on-demand via /api/auth/me atau /api/auth/quantum_token.
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
name: user.email.split('@')[0], // Display name from email prefix
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
tenantName: user.tenantName,
|
||||
allowCrossGroup: user.allowCrossGroup,
|
||||
mediaEngineStrategy: effectiveMediaStrategy,
|
||||
chatEngineStrategy: user.chatEngineStrategy,
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({
|
||||
message: 'Otorisasi Berhasil. Membuka Gerbang...',
|
||||
user: {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantName: user.tenantName,
|
||||
licenses: quantumLicenses,
|
||||
mediaEngineStrategy: effectiveMediaStrategy,
|
||||
chatEngineStrategy: user.chatEngineStrategy
|
||||
}
|
||||
}, { status: 200 });
|
||||
|
||||
// Set HttpOnly Cookie for SSO across subdomains
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true, // KRITIS-1 FIX: HttpOnly true untuk mencegah pencurian token via XSS. Chat menggunakan /c/api/auth/me.
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined, // Dinamis berdasar env/lokal
|
||||
maxAge: 8 * 60 * 60 // 8 hours
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API AUTH ERROR]', error);
|
||||
return NextResponse.json({ error: `Kesalahan Sistem Internal PostgreSQL: ${error?.message || 'Unknown error'}` }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
|
||||
// Hapus cookie jumpa_token dengan maxAge 0 dan domain yang sesuai
|
||||
cookieStore.set('jumpa_token', '', {
|
||||
httpOnly: true, // BARU-S2 FIX: Must match login's httpOnly:true to properly delete cookie
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined, // Mengikuti domain tempat aplikasi berjalan
|
||||
maxAge: 0 // Expire immediately
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Logged out successfully' });
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const cookieHeader = req.headers.get('cookie') || '';
|
||||
const cookies = Object.fromEntries(cookieHeader.split('; ').map(c => c.split('=')));
|
||||
const token = cookies['jumpa_token'];
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string);
|
||||
return NextResponse.json(decoded, { status: 200 });
|
||||
} catch (_error) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import crypto from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { users } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { sessionId, signature, email } = await req.json();
|
||||
|
||||
// TAHAP NANO: Validasi Kriptografis Challenge QR Code
|
||||
if (!sessionId || !signature || !email) {
|
||||
return NextResponse.json({ error: 'Missing challenge parameters' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Server memverifikasi bahwa signature adalah hash dari sessionId + email + secret
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', process.env.JWT_SECRET as string)
|
||||
.update(`${sessionId}:${email}`)
|
||||
.digest('hex');
|
||||
|
||||
if (signature !== expectedSignature) {
|
||||
return NextResponse.json({ error: 'Invalid QR cryptographic signature' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Verifikasi berhasil, generate real JWT token
|
||||
const userResult = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
|
||||
if (userResult.length === 0) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const user = userResult[0];
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: user.tenantId,
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({ message: 'QR Auth Berhasil' });
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 8 * 60 * 60
|
||||
});
|
||||
return response;
|
||||
|
||||
} catch (e) {
|
||||
console.error("QR Auth Verify Error:", e);
|
||||
return NextResponse.json({ error: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// [TSM.ID].[11031972] — All Rights Reserved. Proprietary & Confidential.
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { tenants, users } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { QuantumOrchestrator } from "@/lib/quantum-orchestrator";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as DecodedToken;
|
||||
|
||||
// Fetch tenant data to get BYOK settings and licenses
|
||||
const tenantData = await db.select().from(tenants).where(eq(tenants.id, decoded.tenantId)).limit(1);
|
||||
const userData = await db.select().from(users).where(eq(users.id, decoded.userId)).limit(1);
|
||||
|
||||
if (tenantData.length === 0) {
|
||||
return NextResponse.json({ error: 'Tenant not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const tenant = tenantData[0];
|
||||
const user = userData[0];
|
||||
|
||||
// Parse licenses
|
||||
const rawLicenses = tenant.licenses || '{}';
|
||||
const tenantLicParsed = JSON.parse(rawLicenses);
|
||||
let tenantLicNormalized: Record<string, string> = {};
|
||||
if (Array.isArray(tenantLicParsed)) {
|
||||
tenantLicParsed.forEach((k: string) => tenantLicNormalized[k] = 'GRANTED');
|
||||
} else {
|
||||
tenantLicNormalized = tenantLicParsed || {};
|
||||
}
|
||||
|
||||
const userLicParsed = JSON.parse(user?.licenses || '{}');
|
||||
let userLicNormalized: Record<string, string> = {};
|
||||
if (Array.isArray(userLicParsed)) {
|
||||
userLicParsed.forEach((k: string) => userLicNormalized[k] = 'GRANTED');
|
||||
} else {
|
||||
userLicNormalized = userLicParsed || {};
|
||||
}
|
||||
|
||||
// Resolve 101-Module Matrix Capabilities
|
||||
const capabilities = QuantumOrchestrator.resolve(
|
||||
tenantLicNormalized,
|
||||
userLicNormalized,
|
||||
!!tenant.isActive
|
||||
);
|
||||
|
||||
// Parse legacy modules string list
|
||||
let modules: string[] = [];
|
||||
try {
|
||||
if (Array.isArray(tenantLicParsed)) {
|
||||
modules = tenantLicParsed;
|
||||
} else {
|
||||
modules = Object.keys(tenantLicParsed).filter(k => (tenantLicParsed as Record<string, string>)[k] === 'GRANTED');
|
||||
}
|
||||
} catch (_e) {
|
||||
modules = [];
|
||||
}
|
||||
|
||||
// Determine the active BYOK Key
|
||||
// Priority: User > Tenant
|
||||
let activeByokKey = 'none';
|
||||
let byokLevel = 'SYSTEM';
|
||||
|
||||
if (user?.byokEnabled && user?.byokKey) {
|
||||
activeByokKey = user.byokKey;
|
||||
byokLevel = 'USER';
|
||||
} else if (tenant.byokEnabled && tenant.byokKey) {
|
||||
activeByokKey = tenant.byokKey;
|
||||
byokLevel = 'TENANT';
|
||||
}
|
||||
|
||||
// Secret WAJIB sama dengan XCU_QCG_SECRET di XCU Core (PKX Rule #4)
|
||||
const xcuSecret = process.env.XCU_QCG_SECRET || process.env.XCU_TOKEN_SECRET;
|
||||
if (!xcuSecret) {
|
||||
throw new Error('HUKUM MUTLAK: XCU_QCG_SECRET tidak ditemukan! Gerbang Anti-Jumping menolak akses.');
|
||||
}
|
||||
|
||||
const quantumToken = jwt.sign({
|
||||
sub: decoded.email,
|
||||
tenantId: decoded.tenantId,
|
||||
modules: modules,
|
||||
capabilities: capabilities,
|
||||
byok: activeByokKey,
|
||||
byokLevel: byokLevel,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 12)
|
||||
}, xcuSecret);
|
||||
|
||||
return NextResponse.json({
|
||||
token: quantumToken,
|
||||
modules: modules,
|
||||
capabilities: capabilities,
|
||||
byokActive: activeByokKey !== 'none',
|
||||
byokLevel: byokLevel
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[QUANTUM TOKEN ERROR]', error);
|
||||
return NextResponse.json({ error: 'Quantum Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { email, password } = await req.json();
|
||||
|
||||
if (!email || !password || password.length < 6) {
|
||||
return NextResponse.json({ error: 'Email and password (min 6 chars) are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
const existingUser = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
if (existingUser.length > 0) {
|
||||
return NextResponse.json({ error: 'Email sudah terdaftar.' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Create a new Tenant for the user
|
||||
const tenantName = `Personal Workspace - ${email.split('@')[0]}`;
|
||||
const newTenantResult = await writerDb.insert(tenants).values({
|
||||
name: tenantName,
|
||||
isActive: true
|
||||
}).returning({ id: tenants.id, name: tenants.name });
|
||||
|
||||
const newTenant = newTenantResult[0];
|
||||
|
||||
// Create the user with bcrypt-hashed password (Kelas Militer)
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
const newUserResult = await writerDb.insert(users).values({
|
||||
email: email,
|
||||
passwordHash: hashedPassword,
|
||||
tenantId: newTenant.id,
|
||||
role: 'admin' // The creator of the personal tenant is an admin
|
||||
}).returning({ id: users.id, email: users.email, role: users.role });
|
||||
|
||||
const user = newUserResult[0];
|
||||
|
||||
// Generate JWT Token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantId: newTenant.id,
|
||||
tenantName: newTenant.name,
|
||||
licenses: {}
|
||||
},
|
||||
process.env.JWT_SECRET as string,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const response = NextResponse.json({
|
||||
message: 'Registrasi Berhasil.',
|
||||
user: {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
tenantName: newTenant.name
|
||||
}
|
||||
}, { status: 201 });
|
||||
|
||||
// Set HttpOnly Cookie for SSO
|
||||
response.cookies.set({
|
||||
name: 'jumpa_token',
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
domain: process.env.NEXT_PUBLIC_COOKIE_DOMAIN || undefined,
|
||||
maxAge: 8 * 60 * 60
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[API REGISTER ERROR]', error);
|
||||
return NextResponse.json({ error: 'Kesalahan Sistem Internal PostgreSQL' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { tenants } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
/**
|
||||
* XENDIT WEBHOOK HANDLER (QUANTUM SYNC)
|
||||
*
|
||||
* Menerima callback dari Xendit saat pembayaran Lunas (PAID).
|
||||
* Langsung mengupdate status tenant secara reaktif.
|
||||
*/
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
// Keamanan: Validasi Xendit Callback Token (WAJIB di Production)
|
||||
const callbackToken = req.headers.get('x-callback-token');
|
||||
if (callbackToken !== process.env.XENDIT_CALLBACK_TOKEN) {
|
||||
console.error('[XENDIT WEBHOOK] REJECTED: Invalid callback token');
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { status, external_id, amount, payment_method } = body;
|
||||
|
||||
if (status === 'PAID') {
|
||||
const parts = external_id.split('-');
|
||||
const tenantId = parts[1]; // We should use full ID or mapping table
|
||||
|
||||
console.log(`[XENDIT WEBHOOK] PAID: ${amount} via ${payment_method} for Tenant: ${tenantId}`);
|
||||
|
||||
// Aktifkan Tenant secara otomatis (WAJIB writerDb untuk mutasi)
|
||||
await writerDb.update(tenants)
|
||||
.set({ isActive: true })
|
||||
.where(eq(tenants.id, tenantId));
|
||||
|
||||
console.log(`[SYSTEM] Tenant ${tenantId} has been reactivated via Quantum Billing.`);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("[WEBHOOK ERROR]", error);
|
||||
return NextResponse.json({ error: "Webhook Processing Failed" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { systemFeatures } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const modules = [
|
||||
// VIDEO ENGINE (XCU) - 25 Modules
|
||||
{ name: "XCU AV1 Codec (4K)", key: "xcu.codec.av1", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU HEVC Codec (2K)", key: "xcu.codec.hevc", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU VP9 Optimized", key: "xcu.codec.vp9", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "WebTransport / QUIC", key: "xcu.transport.quic", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "MoQ (Media over QUIC)", key: "xcu.transport.moq", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Auto-Pilot Codec Routing", key: "xcu.feature.autopilot", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Ultra-Low Latency (0ms)", key: "xcu.feature.ull", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Global Mesh Relay", key: "xcu.feature.mesh", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Simulcast Matrix", key: "xcu.feature.simulcast", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "SVC (Scalable Video Coding)", key: "xcu.feature.svc", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "XCU On-Premise Gateway", key: "xcu.feature.onprem", module: "XCU", defaultState: "HIDDEN" },
|
||||
{ name: "Dynamic Bitrate Adaptation", key: "xcu.feature.dba", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Hardware Acceleration", key: "xcu.feature.hwaccel", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Screen Share 60FPS", key: "xcu.feature.screenshare_high", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Virtual Background AI", key: "xcu.feature.vbg_ai", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Noise Suppression AI", key: "xcu.feature.noise_ai", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Echo Cancellation Ultra", key: "xcu.feature.aec_ultra", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Camera Broadcast", key: "xcu.feature.multicam", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Live Canvas TTE", key: "xcu.feature.tte", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Breakout Matrix Rooms", key: "xcu.feature.breakout", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Recording Sovereign", key: "xcu.feature.recording", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Live Streaming RTMP/HLS", key: "xcu.feature.stream_out", module: "XCU", defaultState: "UPSELL" },
|
||||
{ name: "Watermark Dynamic", key: "xcu.feature.watermark", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "Telepathy Sync State", key: "xcu.feature.telepathy", module: "XCU", defaultState: "GRANTED" },
|
||||
{ name: "XCU Kernel Bypass (eBPF)", key: "xcu.feature.ebpf", module: "XCU", defaultState: "HIDDEN" },
|
||||
|
||||
// CHAT ENGINE (XTM) - 25 Modules
|
||||
{ name: "End-to-End Encryption", key: "xtm.security.e2ee", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Omni-Brain Interceptor", key: "xtm.ai.interceptor", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "The Vault (DRM Storage)", key: "xtm.storage.vault", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Self-Destruct Messages", key: "xtm.feature.burn", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Quantum Handshake", key: "xtm.security.quantum", module: "XTM", defaultState: "HIDDEN" },
|
||||
{ name: "Biometric Auth Chat", key: "xtm.security.biometric", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Persistent Message Log", key: "xtm.feature.persistence", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Large File Transfer (10GB)", key: "xtm.feature.file_large", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Global Search Index", key: "xtm.feature.search", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Group Management Pro", key: "xtm.feature.group_pro", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Audit Trail Chat", key: "xtm.feature.audit", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Bot Integration API", key: "xtm.feature.bot", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Reaction Matrix", key: "xtm.feature.reaction", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Threaded Conversations", key: "xtm.feature.threads", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Translate Auto AI", key: "xtm.ai.translate", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Sentiment Analysis", key: "xtm.ai.sentiment", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Summary Neural Link", key: "xtm.ai.summary", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Offline Sync Mode", key: "xtm.feature.offline", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Device Sync", key: "xtm.feature.multidevice", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Whisper Mode", key: "xtm.feature.whisper", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Announcement Channel", key: "xtm.feature.broadcast", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Read Receipts Stealth", key: "xtm.feature.stealth", module: "XTM", defaultState: "UPSELL" },
|
||||
{ name: "Custom Emoji Pack", key: "xtm.feature.emoji", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Voice Note Matrix", key: "xtm.feature.voicenote", module: "XTM", defaultState: "GRANTED" },
|
||||
{ name: "Video Message Circle", key: "xtm.feature.videonote", module: "XTM", defaultState: "UPSELL" },
|
||||
|
||||
// IAM / CORE - 20 Modules
|
||||
{ name: "Supreme Eye Dashboard", key: "iam.feature.supreme_eye", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "BYOK Matrix Control", key: "iam.feature.byok", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "White-Label Branding", key: "iam.feature.branding", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Sovereign VAULT Auth", key: "iam.auth.vault", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Zero-Trust SSO", key: "iam.auth.sso", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Xendit Payment Bridge", key: "iam.billing.xendit", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Multi-Currency Support", key: "iam.billing.currency", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Audit Log Export", key: "iam.security.audit_log", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "IP Whitelisting", key: "iam.security.ip_whitelist", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "License Generator", key: "iam.admin.licenses", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Tenant Isolation Pro", key: "iam.security.isolation", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Advanced Role Matrix", key: "iam.security.roles", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "Usage Telemetry", key: "iam.feature.telemetry", module: "IAM", defaultState: "GRANTED" },
|
||||
{ name: "API Gateway Access", key: "iam.feature.api_access", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Custom Domain Mapping", key: "iam.feature.custom_domain", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "B2B Marketplace", key: "iam.feature.marketplace", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "Developer Sandbox", key: "iam.feature.sandbox", module: "IAM", defaultState: "UPSELL" },
|
||||
{ name: "System Mindmap", key: "iam.feature.mindmap", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Quantum Logs", key: "iam.feature.quantum_logs", module: "IAM", defaultState: "HIDDEN" },
|
||||
{ name: "Emergency Kill-Switch", key: "iam.security.killswitch", module: "IAM", defaultState: "HIDDEN" },
|
||||
|
||||
// JVC (JUMPA Video Conference) UI Controls - 14 Modules
|
||||
{ name: "Tombol Microphone", key: "jvc.ui.microphone", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Tombol Camera + Flip", key: "jvc.ui.camera", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Panel Peserta", key: "jvc.ui.people_panel", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Panel Chat", key: "jvc.ui.chat_panel", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Screen Share", key: "jvc.ui.screenshare", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Emoji Reactions", key: "jvc.ui.reactions", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Breakout Rooms", key: "jvc.ui.breakout", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Recording", key: "jvc.ui.recording", module: "JVC", defaultState: "UPSELL" },
|
||||
{ name: "Neural Radiance Filter", key: "jvc.ui.beauty_filter", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Virtual Background + Bokeh", key: "jvc.ui.virtual_bg", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Meeting Timer", key: "jvc.ui.timer", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "XCO Command Matrix Panel", key: "jvc.ui.matrix_command", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Gallery/Speaker View Toggle", key: "jvc.ui.layout_toggle", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Host Controls (Mute All, Kick)", key: "jvc.ui.host_controls", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Video Engine Selector (XCO)", key: "jvc.xco.video_engine", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "Audio Engine Selector (XCO)", key: "jvc.xco.audio_engine", module: "JVC", defaultState: "GRANTED" },
|
||||
{ name: "FPS Selector (XCO)", key: "jvc.xco.fps_selector", module: "JVC", defaultState: "GRANTED" },
|
||||
|
||||
// JC (JUMPA Chat) UI Controls - 12 Modules
|
||||
{ name: "Kirim Pesan Text", key: "jc.ui.send_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Emoji Reactions Chat", key: "jc.ui.reactions", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "File Upload", key: "jc.ui.file_upload", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Voice Notes", key: "jc.ui.voice_notes", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "OmniBrain AI Chat", key: "jc.ui.omnibrain", module: "JC", defaultState: "UPSELL" },
|
||||
{ name: "Edit Message", key: "jc.ui.edit_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Delete Message", key: "jc.ui.delete_message", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Group Management", key: "jc.ui.group_manage", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Typing Indicator", key: "jc.ui.typing_indicator", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Read Receipts", key: "jc.ui.read_receipts", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Video Call from Chat", key: "jc.ui.video_call", module: "JC", defaultState: "GRANTED" },
|
||||
{ name: "Voice Call from Chat", key: "jc.ui.voice_call", module: "JC", defaultState: "GRANTED" },
|
||||
];
|
||||
|
||||
// Seed logic
|
||||
for (const m of modules) {
|
||||
const existing = await db.select().from(systemFeatures).where(eq(systemFeatures.key, m.key)).limit(1);
|
||||
if (existing.length === 0) {
|
||||
await writerDb.insert(systemFeatures).values(m);
|
||||
} else {
|
||||
// Update to ensure defaults are correct
|
||||
await writerDb.update(systemFeatures).set({ name: m.name, module: m.module }).where(eq(systemFeatures.key, m.key));
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `${modules.length} Quantum Modules Synchronized`,
|
||||
count: modules.length
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "Internal Error during Seeding";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Klien Redis untuk Publisher
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { channel, event, payload } = body;
|
||||
|
||||
if (!channel || !event) {
|
||||
return NextResponse.json({ error: "Missing channel or event" }, { status: 400 });
|
||||
}
|
||||
|
||||
// IAM tidak menangani host_approve_guest secara langsung dengan DB pg
|
||||
|
||||
const message = JSON.stringify({ event, payload, timestamp: Date.now() });
|
||||
|
||||
// Pancarkan ke Redis PubSub, yang akan ditangkap oleh endpoint SSE
|
||||
await redis.publish(channel, message);
|
||||
|
||||
console.log(`[EMIT] Memancarkan '${event}' ke '${channel}'`);
|
||||
|
||||
return NextResponse.json({ success: true, message: "Signal transmitted" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Gagal memancarkan sinyal";
|
||||
console.error("[EMIT] Gagal memancarkan sinyal:", message);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { history, prompt, sender } = body;
|
||||
|
||||
// Environment variables routing
|
||||
const engine = process.env.OMNIBRAIN_ENGINE || "XCU_DEEP_CORE";
|
||||
const openaiKey = process.env.OPENAI_API_KEY;
|
||||
const geminiKey = process.env.GEMINI_API_KEY;
|
||||
const grokKey = process.env.GROK_API_KEY;
|
||||
|
||||
// Proxy URLs for State Audit Compliance
|
||||
const openaiUrl = process.env.OPENAI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const geminiUrl = process.env.GEMINI_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
const grokUrl = process.env.GROK_API_URL || "http://127.0.0.1:11434/v1/chat/completions";
|
||||
|
||||
// Format history for context
|
||||
const historyText = history.map((m: { sender: string; text: string }) => `${m.sender}: ${m.text}`).join('\n');
|
||||
const systemPrompt = 'You are Omni-Brain, an advanced AI telepathic observer inside an End-to-End Encrypted XCU Ultra chat room. Provide concise, highly analytical, and awe-inspiring responses in Indonesian. Use terms like "Jaringan Kuantum", "Matriks Telepati", etc.';
|
||||
const userPrompt = `[Decrypted Context - Top Secret]\n${historyText}\n\nUser [${sender}] commands: ${prompt}`;
|
||||
|
||||
let reply = "";
|
||||
|
||||
// DYNAMIC ROUTING ENGINE
|
||||
if (engine === "OPENAI" && openaiKey) {
|
||||
const response = await fetch(openaiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${openaiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'gpt-4o',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
],
|
||||
max_tokens: 300
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else if (engine === "GEMINI" && geminiKey) {
|
||||
const response = await fetch(geminiUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: `${systemPrompt}\n\n${userPrompt}` }] }]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
||||
|
||||
} else if (engine === "GROK" && grokKey) {
|
||||
const response = await fetch(grokUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${grokKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'grok-beta',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
|
||||
} else {
|
||||
// 1000% DEFAULT FALLBACK: XCU DEEP-CORE LOCAL LLM (OLLAMA / Llama3)
|
||||
// Jaringan Air-Gapped Sovereign murni
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:11434/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'llama3', // Atau deepseek-coder, mistral, dll.
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: userPrompt }
|
||||
]
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
reply = data.choices?.[0]?.message?.content || "";
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ reply: "Sistem XCU Deep-Core LLM terputus. Pastikan daemon Ollama berjalan di peladen (127.0.0.1:11434)." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return NextResponse.json({ reply }, { status: 200 });
|
||||
} else {
|
||||
return NextResponse.json({ reply: "OmniBrain mengalami anomali dalam matriks kuantum. Gagal memproses data." }, { status: 200 });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('OmniBrain Error:', error);
|
||||
return NextResponse.json({ reply: "Terjadi distorsi telepatik saat memproses neural-link." }, { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import Redis from "ioredis";
|
||||
|
||||
// Klien Redis khusus untuk Subscriber (Mendengarkan event OmniBrain)
|
||||
// Karena ini adalah endpoint streaming, kita membutuhkan instance Redis tersendiri per koneksi (opsional),
|
||||
// namun karena SSE bersifat long-lived, kita instansiasi dalam blok handler.
|
||||
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
const channel = searchParams.get("channel"); // Misalnya: "room_alpha_events"
|
||||
|
||||
if (!channel) {
|
||||
return NextResponse.json({ error: "Missing channel" }, { status: 400 });
|
||||
}
|
||||
|
||||
const redis = new Redis(redisUrl);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
// 1. Subscribe ke channel spesifik di Redis
|
||||
await redis.subscribe(channel, (err, _count) => {
|
||||
if (err) {
|
||||
console.error("[SSE] Gagal subscribe ke Redis:", err);
|
||||
controller.error(err);
|
||||
} else {
|
||||
console.log(`[SSE] OmniBrain terhubung ke saluran: ${channel}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Dengarkan pesan yang dipancarkan oleh API /emit
|
||||
redis.on("message", (subChannel, message) => {
|
||||
if (subChannel === channel) {
|
||||
// Format SSE: "data: {JSON}\n\n"
|
||||
controller.enqueue(`data: ${message}\n\n`);
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Keep-Alive (Mencegah koneksi HTTP ditutup oleh Proxy/Nginx)
|
||||
const keepAlive = setInterval(() => {
|
||||
controller.enqueue(":\n\n"); // Komentar SSE untuk keep-alive
|
||||
}, 15000);
|
||||
|
||||
// 4. Deteksi klien putus
|
||||
req.signal.addEventListener("abort", () => {
|
||||
clearInterval(keepAlive);
|
||||
redis.disconnect();
|
||||
console.log(`[SSE] Klien terputus dari saluran: ${channel}`);
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
redis.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return new NextResponse(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, userId, targetTenantId, newRole, newTenantName } = body;
|
||||
|
||||
const userRecord = await db.select().from(users).where(eq(users.id, userId));
|
||||
if (!userRecord.length) return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
||||
|
||||
if (action === 'TRANSFER') {
|
||||
await writerDb.update(users)
|
||||
.set({ tenantId: targetTenantId, role: newRole })
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CROSS_USER_TRANSFER',
|
||||
targetId: userId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'PROMOTE') {
|
||||
const [newTenant] = await writerDb.insert(tenants).values({
|
||||
name: newTenantName,
|
||||
isActive: true,
|
||||
}).returning();
|
||||
|
||||
await writerDb.update(users)
|
||||
.set({ tenantId: newTenant.id, role: 'admin' })
|
||||
.where(eq(users.id, userId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CROSS_USER_PROMOTE',
|
||||
targetId: userId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, newTenant });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { quantumLogs } from "@/drizzle/schema";
|
||||
import { desc, inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const logs = await db.select().from(quantumLogs).orderBy(desc(quantumLogs.nanoTimestamp)).limit(100);
|
||||
return NextResponse.json({ logs });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
if (!body.ids || !Array.isArray(body.ids)) return NextResponse.json({ error: 'Bad Request' }, { status: 400 });
|
||||
|
||||
await writerDb.delete(quantumLogs).where(inArray(quantumLogs.id, body.ids));
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { type, tenantName, userEmail, userPassword, userRole, tenantId } = body;
|
||||
|
||||
if (type === 'TENANT') {
|
||||
if (!tenantName) return NextResponse.json({ error: 'Tenant Name required' }, { status: 400 });
|
||||
await writerDb.insert(tenants).values({
|
||||
name: tenantName,
|
||||
isActive: true
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MATRIX_ADD_TENANT',
|
||||
targetId: tenantName,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} else if (type === 'USER') {
|
||||
if (!userEmail || !userPassword || !tenantId) {
|
||||
return NextResponse.json({ error: 'Email, Password, and Tenant ID are required' }, { status: 400 });
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(userPassword, 12);
|
||||
await writerDb.insert(users).values({
|
||||
email: userEmail,
|
||||
passwordHash: hashedPassword,
|
||||
tenantId: tenantId,
|
||||
role: userRole || 'user'
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MATRIX_ADD_USER',
|
||||
targetId: userEmail,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { userIds, tenantIds, confirmation } = await req.json();
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
return NextResponse.json({ error: 'Invalid Confirmation Keyword' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (userIds && userIds.length > 0) {
|
||||
await writerDb.delete(users).where(inArray(users.id, userIds));
|
||||
}
|
||||
|
||||
if (tenantIds && tenantIds.length > 0) {
|
||||
await writerDb.delete(users).where(inArray(users.tenantId, tenantIds));
|
||||
await writerDb.delete(tenants).where(inArray(tenants.id, tenantIds));
|
||||
}
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MASS_BULK_KILL',
|
||||
targetId: `Users:${userIds?.length||0}, Tenants:${tenantIds?.length||0}`,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Bulk Eradication Complete' });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { type, id, newValue, role, password, engineStrategy, chatEngineStrategy } = body;
|
||||
|
||||
if (type === 'TENANT') {
|
||||
const updatePayload: Record<string, string> = { name: newValue };
|
||||
if (engineStrategy) updatePayload.mediaEngineStrategy = engineStrategy;
|
||||
if (chatEngineStrategy) updatePayload.chatEngineStrategy = chatEngineStrategy;
|
||||
await writerDb.update(tenants).set(updatePayload).where(eq(tenants.id, id));
|
||||
} else if (type === 'USER') {
|
||||
const updateData: Record<string, string> = { email: newValue };
|
||||
if (role) updateData.role = role;
|
||||
if (password && password.trim() !== '') {
|
||||
updateData.passwordHash = await bcrypt.hash(password, 12);
|
||||
}
|
||||
|
||||
await writerDb.update(users).set(updateData).where(eq(users.id, id));
|
||||
} else {
|
||||
return NextResponse.json({ error: 'Invalid type' }, { status: 400 });
|
||||
}
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: `MATRIX_INLINE_EDIT_${type}`,
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { saasPackages, quantumLogs, systemFeatures } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const packages = await db.select().from(saasPackages);
|
||||
const systemFeaturesList = await db.select().from(systemFeatures);
|
||||
return NextResponse.json({ packages, systemFeatures: systemFeaturesList });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { action, id, name, price, features, isHidden } = await req.json();
|
||||
|
||||
if (action === 'create') {
|
||||
await writerDb.insert(saasPackages).values({
|
||||
name,
|
||||
price,
|
||||
features: JSON.stringify(features),
|
||||
isHidden: !!isHidden
|
||||
});
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'CREATE_PACKAGE',
|
||||
targetId: name,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
await writerDb.update(saasPackages)
|
||||
.set({
|
||||
name,
|
||||
price,
|
||||
features: JSON.stringify(features),
|
||||
isHidden: !!isHidden
|
||||
})
|
||||
.where(eq(saasPackages.id, id));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'UPDATE_PACKAGE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
await writerDb.delete(saasPackages).where(eq(saasPackages.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'DELETE_PACKAGE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { type, id, confirmation } = await req.json();
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
return NextResponse.json({ error: 'Invalid Confirmation Keyword' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type === 'USER') {
|
||||
await writerDb.delete(users).where(eq(users.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SURGICAL_KILL_USER',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true, message: 'User Terminated' });
|
||||
}
|
||||
|
||||
if (type === 'TENANT') {
|
||||
await writerDb.delete(users).where(eq(users.tenantId, id));
|
||||
await writerDb.delete(tenants).where(eq(tenants.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'MASS_KILL_TENANT',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true, message: 'Tenant and all its users Terminated' });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Type' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const CA_CERT_PATH = '/etc/xcu-sovereign-ca/ca.crt';
|
||||
|
||||
// GET: Download the Sovereign CA certificate
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin' && decoded.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(CA_CERT_PATH)) {
|
||||
return NextResponse.json({ error: 'CA Certificate not generated yet. Contact Supreme Admin.' }, { status: 404 });
|
||||
}
|
||||
|
||||
const certData = fs.readFileSync(CA_CERT_PATH);
|
||||
return new NextResponse(certData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-x509-ca-cert',
|
||||
'Content-Disposition': 'attachment; filename="xcu-sovereign-ca.crt"',
|
||||
'Cache-Control': 'no-store',
|
||||
},
|
||||
});
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, messages, quantumLogs, saasPackages } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import os from 'os';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Access Denied: Supreme Mode Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Server Health
|
||||
const serverVitals = {
|
||||
cpuCount: os.cpus().length,
|
||||
cpuModel: os.cpus()[0]?.model || 'Unknown',
|
||||
totalMemMB: Math.round(os.totalmem() / 1024 / 1024),
|
||||
freeMemMB: Math.round(os.freemem() / 1024 / 1024),
|
||||
uptimeSecs: Math.round(os.uptime())
|
||||
};
|
||||
|
||||
// 2. Metrics
|
||||
// Using simple count logic by pulling array length or specific aggregations.
|
||||
// For pure Postgres counts, we can do direct selects.
|
||||
const allUsersCountResult = await db.execute(sql`SELECT count(*) FROM users`);
|
||||
const allTenantsCountResult = await db.execute(sql`SELECT count(*) FROM tenants`);
|
||||
const allMessagesCountResult = await db.execute(sql`SELECT count(*) FROM messages`);
|
||||
|
||||
const totalUsers = parseInt(allUsersCountResult[0].count as string);
|
||||
const totalTenants = parseInt(allTenantsCountResult[0].count as string);
|
||||
const totalMessages = parseInt(allMessagesCountResult[0].count as string);
|
||||
|
||||
// 3. Omni-Penetration Matrix (Limit top 50 tenants for dashboard performance)
|
||||
const allTenants = await db.select().from(tenants).limit(50);
|
||||
const tenantIds = allTenants.map(t => t.id);
|
||||
const allPackages = await db.select().from(saasPackages);
|
||||
|
||||
// We fetch users for each displayed tenant to show in Supreme Admin
|
||||
const allUsers = await db.select({
|
||||
id: users.id,
|
||||
email: users.email,
|
||||
role: users.role,
|
||||
tenantId: users.tenantId,
|
||||
}).from(users);
|
||||
|
||||
const matrix = allTenants.map(tenant => {
|
||||
const tenantPackage = allPackages.find(p => p.id === tenant.packageId) || null;
|
||||
const tenantUsers = allUsers.filter(u => u.tenantId === tenant.id);
|
||||
return {
|
||||
...tenant,
|
||||
package: tenantPackage,
|
||||
users: tenantUsers
|
||||
};
|
||||
});
|
||||
|
||||
// 4. Record the quantum log (wrap in try-catch for read-replicas)
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'OMNI_SIGHT_ACCESS',
|
||||
targetId: 'ALL_SYSTEMS',
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
serverVitals,
|
||||
metrics: {
|
||||
totalUsers: totalUsers,
|
||||
totalTenants: totalTenants,
|
||||
totalMessages: totalMessages
|
||||
},
|
||||
matrix
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, tenantId, licenses, byokEnabled, byokKey } = body;
|
||||
|
||||
if (action === 'update_tenant_licenses') {
|
||||
const updateData: { licenses: string; byokEnabled?: boolean; byokKey?: string } = {
|
||||
licenses: JSON.stringify(licenses)
|
||||
};
|
||||
|
||||
if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled;
|
||||
if (typeof byokKey === 'string') updateData.byokKey = byokKey;
|
||||
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_MATRIX_UPDATE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update_tenant_package') {
|
||||
const { packageId } = body;
|
||||
await writerDb.update(tenants).set({ packageId: packageId || null }).where(eq(tenants.id, tenantId));
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_PACKAGE_ASSIGN',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log (likely read replica):', logError);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
if (action === 'update_security_tier') {
|
||||
const { securityTier } = body;
|
||||
if (!['STANDARD', 'SOVEREIGN', 'CLIENT_CA'].includes(securityTier)) {
|
||||
return NextResponse.json({ error: 'Invalid security tier' }, { status: 400 });
|
||||
}
|
||||
await writerDb.update(tenants).set({ securityTier }).where(eq(tenants.id, tenantId));
|
||||
try {
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: `SECURITY_TIER_SWITCH_${securityTier}`,
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
} catch (logError) {
|
||||
console.warn('[SUPREME EYE] Could not insert quantum log:', logError);
|
||||
}
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Action' }, { status: 400 });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[SUPREME EYE POST ERROR]', error);
|
||||
return NextResponse.json({ error: error.message || 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { users, tenants, messages, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import os from 'os';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Access Denied: Supreme Mode Required' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Server Health
|
||||
const serverVitals = {
|
||||
cpuCount: os.cpus().length,
|
||||
cpuModel: os.cpus()[0]?.model || 'Unknown',
|
||||
totalMemMB: Math.round(os.totalmem() / 1024 / 1024),
|
||||
freeMemMB: Math.round(os.freemem() / 1024 / 1024),
|
||||
uptimeSecs: Math.round(os.uptime())
|
||||
};
|
||||
|
||||
// 2. Metrics (READ — menggunakan db)
|
||||
const allUsers = await db.select().from(users);
|
||||
const allTenants = await db.select().from(tenants);
|
||||
const allMessages = await db.select().from(messages);
|
||||
|
||||
// 3. Omni-Penetration Matrix (Tenants + their users)
|
||||
const matrix = allTenants.map(tenant => {
|
||||
const tenantUsers = allUsers.filter(u => u.tenantId === tenant.id);
|
||||
return {
|
||||
...tenant,
|
||||
users: tenantUsers
|
||||
};
|
||||
});
|
||||
|
||||
// 4. Record the quantum log (WRITE — WAJIB writerDb)
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'OMNI_SIGHT_ACCESS',
|
||||
targetId: 'ALL_SYSTEMS',
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
serverVitals,
|
||||
metrics: {
|
||||
totalUsers: allUsers.length,
|
||||
totalTenants: allTenants.length,
|
||||
totalMessages: allMessages.length
|
||||
},
|
||||
matrix
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const body = await req.json();
|
||||
const { action, tenantId, licenses, byokEnabled, byokKey } = body;
|
||||
|
||||
if (action === 'update_tenant_licenses') {
|
||||
const updateData: { licenses: string; byokEnabled?: boolean; byokKey?: string } = {
|
||||
licenses: JSON.stringify(licenses)
|
||||
};
|
||||
|
||||
if (typeof byokEnabled === 'boolean') updateData.byokEnabled = byokEnabled;
|
||||
if (typeof byokKey === 'string') updateData.byokKey = byokKey;
|
||||
|
||||
// FIXED: Static import (not dynamic), writerDb (not db)
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'SUPREME_MATRIX_UPDATE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Action' }, { status: 400 });
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME EYE POST ERROR]', error);
|
||||
return NextResponse.json({ error: 'Internal System Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { systemFeatures, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
// AUTO-SCANNER ENGINE
|
||||
const manifests = [
|
||||
{ module: 'JC', path: path.join(process.cwd(), '../c/quantum.manifest.json') },
|
||||
{ module: 'JVC', path: path.join(process.cwd(), '../vc/quantum.manifest.json') },
|
||||
{ module: 'IAM', path: path.join(process.cwd(), 'quantum.manifest.json') }
|
||||
];
|
||||
|
||||
const currentFeatures = await db.select().from(systemFeatures);
|
||||
const existingKeys = new Set(currentFeatures.map(f => f.key));
|
||||
|
||||
for (const m of manifests) {
|
||||
try {
|
||||
if (fs.existsSync(m.path)) {
|
||||
const content = fs.readFileSync(m.path, 'utf8');
|
||||
const features = JSON.parse(content);
|
||||
|
||||
// FASE 12: Auto-Migration Legacy Features from JC to JVC
|
||||
if (m.module === 'JVC') {
|
||||
try {
|
||||
await writerDb.update(systemFeatures)
|
||||
.set({ module: 'JVC' })
|
||||
.where(inArray(systemFeatures.name, [
|
||||
"Supreme's Eye (Multiverse)",
|
||||
"Chronos Smart Scheduler",
|
||||
"The Vault (Recordings)",
|
||||
"Omniversal Multi-Stream",
|
||||
"Ultra Breakout Matrix",
|
||||
"Omniversal Multi-Stream & Ultra Breakout Matrix"
|
||||
]));
|
||||
} catch (migErr) {
|
||||
console.error("[AUTO-MIGRATION] Failed:", migErr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const feat of features) {
|
||||
if (!existingKeys.has(feat.key)) {
|
||||
await writerDb.insert(systemFeatures).values({
|
||||
module: m.module,
|
||||
key: feat.key,
|
||||
name: feat.name,
|
||||
description: feat.description || '',
|
||||
defaultState: feat.defaultState || 'UPSELL'
|
||||
});
|
||||
existingKeys.add(feat.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (scanErr) {
|
||||
console.error(`[AUTO-SCANNER] Failed to parse manifest ${m.path}:`, scanErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Refetch after scanning
|
||||
const finalFeatures = await db.select().from(systemFeatures);
|
||||
return NextResponse.json({ features: finalFeatures });
|
||||
|
||||
} catch (_e) {
|
||||
console.error("[SYSTEM FEATURES GET ERROR]", _e);
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { action, id } = await req.json();
|
||||
|
||||
if (action === 'delete') {
|
||||
await writerDb.delete(systemFeatures).where(eq(systemFeatures.id, id));
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'DELETE_PARTICLE',
|
||||
targetId: id,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { networkTelemetry, quantumLogs, liveKillSwitches } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { desc, count, sql } from 'drizzle-orm';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
interface DecodedToken {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// PANOPTICON: Supreme Admin Telemetry API (Global View)
|
||||
// Returns real data from network_telemetry and quantum_logs tables.
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as DecodedToken;
|
||||
if (decoded.role !== 'superadmin') {
|
||||
return NextResponse.json({ error: 'Akses Absolut Diperlukan' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 1. Recent telemetry entries (last 50)
|
||||
const recentTelemetry = await db.select().from(networkTelemetry)
|
||||
.orderBy(desc(networkTelemetry.timestamp))
|
||||
.limit(50);
|
||||
|
||||
// 2. Recent quantum logs (last 100)
|
||||
const recentLogs = await db.select().from(quantumLogs)
|
||||
.orderBy(desc(quantumLogs.nanoTimestamp))
|
||||
.limit(100);
|
||||
|
||||
// 3. Active kill switches
|
||||
const activeKills = await db.select().from(liveKillSwitches);
|
||||
|
||||
// 4. Aggregated stats
|
||||
const [telemetryCount] = await db.select({ total: count() }).from(networkTelemetry);
|
||||
const [logsCount] = await db.select({ total: count() }).from(quantumLogs);
|
||||
const [killsCount] = await db.select({ total: count() }).from(liveKillSwitches);
|
||||
|
||||
// 5. Bandwidth aggregate (sum of traffic bytes from last 24h)
|
||||
const bandwidthAgg = await db.select({
|
||||
totalBytes: sql<string>`COALESCE(SUM(CAST(traffic_bytes AS BIGINT)), 0)`,
|
||||
avgResponseMs: sql<string>`COALESCE(AVG(CAST(response_time_ms AS NUMERIC)), 0)`,
|
||||
}).from(networkTelemetry);
|
||||
|
||||
return NextResponse.json({
|
||||
telemetry: recentTelemetry,
|
||||
logs: recentLogs,
|
||||
kills: activeKills,
|
||||
stats: {
|
||||
totalTelemetryRecords: telemetryCount?.total || 0,
|
||||
totalLogRecords: logsCount?.total || 0,
|
||||
activeKillSwitches: killsCount?.total || 0,
|
||||
totalBandwidthBytes: bandwidthAgg[0]?.totalBytes || '0',
|
||||
avgResponseTimeMs: parseFloat(String(bandwidthAgg[0]?.avgResponseMs || '0')).toFixed(2),
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('[SUPREME TELEMETRY ERROR]', error);
|
||||
return NextResponse.json({ error: 'Telemetry Sync Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { writerDb } from "@/drizzle/db";
|
||||
import { tenants, quantumLogs } from "@/drizzle/schema";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { email: string; role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const { tenantId, licenses, platformName, brandColor, allowCrossGroup } = await req.json();
|
||||
|
||||
const updateData: Record<string, string | boolean> = {};
|
||||
if (licenses !== undefined) updateData.licenses = JSON.stringify(licenses);
|
||||
if (platformName !== undefined) updateData.platformName = platformName;
|
||||
if (brandColor !== undefined) updateData.brandColor = brandColor;
|
||||
if (allowCrossGroup !== undefined) updateData.allowCrossGroup = allowCrossGroup;
|
||||
|
||||
await writerDb.update(tenants).set(updateData).where(eq(tenants.id, tenantId));
|
||||
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
actor: decoded.email,
|
||||
action: 'QUANTUM_MODIFIER_OVERRIDE',
|
||||
targetId: tenantId,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent') || 'Unknown'
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// 99 MODUL XCU MUTLAK — Ini adalah kebenaran, bukan dummy
|
||||
const XCU_MODULES_REGISTRY = [
|
||||
// KASTA ALPHA: PONDASI INTI (Fase 1 - 20)
|
||||
{ id: 'xcu.p01', name: 'Cerberus Quantum Firewall', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p02', name: 'Adaptive Rate Limiter', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p03', name: 'IP Reputation Matrix', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p04', name: 'TLS 1.3 Terminator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p05', name: 'eBPF/XDP Kernel Bypass', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p06', name: 'Zero-Copy Socket Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p07', name: 'DPDK Packet Accelerator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p08', name: 'Ring Buffer Allocator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p09', name: 'NUMA-Aware Scheduler', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p10', name: 'Glommio Thread-per-Core', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p11', name: 'io_uring Async I/O', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p12', name: 'Memory-Mapped File Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p13', name: 'TCP BBR Congestion Control', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p14', name: 'SCTP Multi-Stream Transport', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p15', name: 'WebTransport & QUIC Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p16', name: 'HTTP/3 Gateway', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p17', name: 'ICE/STUN/TURN Orchestrator', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p18', name: 'mDNS Service Discovery', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p19', name: 'Anycast Routing Engine', kelompok: 'KELOMPOK I' },
|
||||
{ id: 'xcu.p20', name: 'Dual-Universe Nginx Gateway', kelompok: 'KELOMPOK I' },
|
||||
|
||||
// KASTA BETA: MANAJEMEN & GUI (Fase 21 - 40)
|
||||
{ id: 'xcu.p21', name: 'Quantum Tollgate / DuckDB Billing', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p22', name: 'Bandwidth Metering Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p23', name: 'Usage Analytics Pipeline', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p24', name: 'Real-Time Invoice Generator', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p25', name: 'Holographic Telemetry Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p26', name: 'Canvas Particle Visualizer', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p27', name: 'WebGL Metrics Renderer', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p28', name: 'Server Heartbeat Monitor', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p29', name: 'Distributed Tracing Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p30', name: 'Zero-Database JWT IAM Gatekeeper', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p31', name: 'RBAC Policy Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p32', name: 'Session Fingerprint Validator', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p33', name: 'Cassandra Matrix / Necro-Computing', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p34', name: 'Hot-Standby Replica Sync', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p35', name: 'WAL (Write-Ahead Log) Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p36', name: 'Schema Migration Autopilot', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p37', name: 'Config Hot-Reload Engine', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p38', name: 'Feature Flag Controller', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p39', name: 'A/B Testing Framework', kelompok: 'KELOMPOK II' },
|
||||
{ id: 'xcu.p40', name: 'Shapeshifting UI & React DOM', kelompok: 'KELOMPOK II' },
|
||||
|
||||
// KASTA GAMMA: INTELIJEN & DATA (Fase 41 - 60)
|
||||
{ id: 'xcu.p41', name: 'Data Lake Ingestion Pipeline', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p42', name: 'Columnar Storage Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p43', name: 'Bloom Filter Index', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p44', name: 'HyperLogLog Cardinality', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p45', name: 'Macro Aggregate Provincial Ingestion', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p46', name: 'Arrow IPC Serializer', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p47', name: 'WASM Query Executor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p48', name: 'Predicate Pushdown Optimizer', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p49', name: 'Vectorized Execution Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p50', name: 'DuckDB In-Memory OLAP', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p51', name: 'Time-Series Compressor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p52', name: 'Geospatial Index Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p53', name: 'Full-Text Search Inverter', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p54', name: 'Graph Traversal Engine', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p55', name: 'AI-Ready Policy Sandbox', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p56', name: 'ML Model Inference Runtime', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p57', name: 'Neural Embedding Store', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p58', name: 'Anomaly Detection Pipeline', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p59', name: 'Predictive Scaling Advisor', kelompok: 'KELOMPOK III' },
|
||||
{ id: 'xcu.p60', name: 'Omnilingual i18n Engine', kelompok: 'KELOMPOK III' },
|
||||
|
||||
// KASTA OMEGA: KIAMAT & KEBANGKITAN (Fase 61 - 74)
|
||||
{ id: 'xcu.p61', name: 'Crypto Wallet Connector', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p62', name: 'Fiat-to-Crypto Bridge', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p63', name: 'Smart Contract Invoker', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p64', name: 'On-Chain Audit Trail', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p65', name: 'Multi-Currency Crypto Billing', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p66', name: 'Biometric Liveness Detector', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p67', name: 'Voice Print Authenticator', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p68', name: 'Gaze Tracking Anti-Spoof', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p69', name: 'Frame Integrity Hasher', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p70', name: 'Aegis Synthetica Deepfake Annihilator', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p71', name: 'Post-Quantum Ratchet Protocol', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p72', name: 'Omni-Gateway Neural Chat', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p73', name: 'Ephemeral Message Vault', kelompok: 'KELOMPOK IV' },
|
||||
{ id: 'xcu.p74', name: 'Ouroboros Protocol / Absolute Death', kelompok: 'KELOMPOK IV' },
|
||||
|
||||
// KASTA TRANSCENDENCE: MULTI-VPS OMNIPRESENCE (Fase 75 - 87)
|
||||
{ id: 'xcu.p75', name: 'Neural Relay Lintas-VPS', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p76', name: 'Redis Pub/Sub Mesh Bridge', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p77', name: 'NATS JetStream Connector', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p78', name: 'Raft Consensus Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p79', name: 'Gossip Protocol Propagator', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p80', name: 'IAM Cross-Tenant Gatekeeper', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p81', name: 'Multi-Region DNS Failover', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p82', name: 'Edge CDN Injector', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p83', name: 'Blue-Green Deployment Controller', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p84', name: 'Canary Release Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p85', name: 'Circuit Breaker Pattern', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p86', name: 'Bulkhead Isolation Engine', kelompok: 'KELOMPOK V' },
|
||||
{ id: 'xcu.p87', name: 'Chaos Engineering Simulator', kelompok: 'KELOMPOK V' },
|
||||
|
||||
// KASTA SUPREME: KERNEL IMMORTALITY (Fase 88 - 99)
|
||||
{ id: 'xcu.p88', name: 'eBPF Shield Kernel Bypass', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p89', name: 'XDP Firewall Rules Compiler', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p90', name: 'Syscall Intercept Engine', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p91', name: 'Seccomp Sandbox Enforcer', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p92', name: 'Namespace Isolation Matrix', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p93', name: 'cgroup Resource Governor', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p94', name: 'Immutable Infrastructure Engine', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p95', name: 'Binary Attestation Verifier', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p96', name: 'React UI Server-Side Injection', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p97', name: 'Encrypted Build Artifact Sealer', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p98', name: 'Hot-Swap Binary Loader', kelompok: 'KELOMPOK VI' },
|
||||
{ id: 'xcu.p99', name: 'Ouroboros Daemon Self-Healing', kelompok: 'KELOMPOK VI' },
|
||||
];
|
||||
|
||||
export async function GET(_req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
// Try reading real XCU State from multiple possible paths
|
||||
const possiblePaths = [
|
||||
'/var/www/xcom-ultra/xcu-omni-relay/xcu_state.json',
|
||||
path.resolve(process.cwd(), '../xcom-ultra/xcu-omni-relay/xcu_state.json'),
|
||||
path.resolve(process.cwd(), '../../xcom-ultra/xcu-omni-relay/xcu_state.json'),
|
||||
];
|
||||
|
||||
let stateData: Record<string, Record<string, number>> | null = null;
|
||||
for (const p of possiblePaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
try {
|
||||
stateData = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
||||
break;
|
||||
} catch (_parseErr) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Use real metrics if available, else baseline
|
||||
const baseRx = stateData?.metrics?.rx_datagrams || 0;
|
||||
const baseTx = stateData?.metrics?.tx_datagrams || 0;
|
||||
const baseSouls = stateData?.metrics?.active_participants || 0;
|
||||
const baseCpu = stateData?.metrics?.cpu_usage || 0;
|
||||
|
||||
// Only use real metrics (No Fake/Dummy injections allowed by PKX)
|
||||
const liveRx = baseRx;
|
||||
const liveTx = baseTx;
|
||||
|
||||
return NextResponse.json({
|
||||
status: 'CONNECTED',
|
||||
telemetry: {
|
||||
rx_datagrams: liveRx,
|
||||
tx_datagrams: liveTx,
|
||||
participants: baseSouls,
|
||||
cpu_usage: baseCpu
|
||||
},
|
||||
modules: XCU_MODULES_REGISTRY
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("[XCU TELEMETRY ERROR]", e);
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const XCU_NODES = [
|
||||
{ name: 'ALPHA', host: '160.187.143.253' },
|
||||
{ name: 'BETA', host: '160.187.143.133' },
|
||||
{ name: 'GAMMA', host: '160.187.143.172' },
|
||||
];
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
async function fetchNodeStatus(node: typeof XCU_NODES[0]) {
|
||||
const hosts = ['127.0.0.1', node.host];
|
||||
for (const h of hosts) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2500);
|
||||
try {
|
||||
const [certResp, telResp] = await Promise.all([
|
||||
fetch(`http://${h}:8081/api/v1/system/cert`, { signal: controller.signal }),
|
||||
fetch(`http://${h}:8081/api/v1/telemetry/snapshot`, { signal: controller.signal }),
|
||||
]);
|
||||
clearTimeout(timeout);
|
||||
const cert = await certResp.json();
|
||||
const tel = await telResp.json();
|
||||
return {
|
||||
name: node.name, host: node.host, online: true,
|
||||
certHash: cert.hash || 'UNKNOWN', tlsMode: cert.tlsMode || 'LETSENCRYPT',
|
||||
cpu: tel.cpu_usage, ram: tel.ram_usage, status: tel.status,
|
||||
};
|
||||
} catch {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
return { name: node.name, host: node.host, online: false, certHash: null, tlsMode: null, cpu: 0, ram: 0, status: 'OFFLINE' };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { role: string };
|
||||
if (decoded.role !== 'superadmin') return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
|
||||
const results = await Promise.allSettled(XCU_NODES.map(fetchNodeStatus));
|
||||
const nodes = results.map(r => r.status === 'fulfilled' ? r.value : { name: '?', online: false });
|
||||
return NextResponse.json({ nodes });
|
||||
} catch (_e) {
|
||||
return NextResponse.json({ error: 'Internal Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from "@/drizzle/db";
|
||||
import { liveKillSwitches } from "@/drizzle/schema";
|
||||
|
||||
// PANOPTICON: Internal Kill List Endpoint
|
||||
// Called by middleware (proxy.ts) to sync the in-memory kill cache.
|
||||
// Protected by x-internal-secret header matching JWT_SECRET.
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const internalSecret = req.headers.get('x-internal-secret');
|
||||
if (internalSecret !== process.env.JWT_SECRET) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch all active kill targets (not expired)
|
||||
const kills = await db.select({
|
||||
targetId: liveKillSwitches.targetId,
|
||||
}).from(liveKillSwitches);
|
||||
|
||||
const targets = kills.map(k => k.targetId);
|
||||
|
||||
return NextResponse.json({ targets });
|
||||
} catch (error: any) {
|
||||
console.error('[KILL-LIST ERROR]', error);
|
||||
return NextResponse.json({ targets: [] });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db, writerDb } from "@/drizzle/db";
|
||||
import { liveKillSwitches, quantumLogs } from "@/drizzle/schema";
|
||||
import { cookies } from 'next/headers';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// PANOPTICON: Live Kill Endpoint
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('jumpa_token')?.value;
|
||||
|
||||
if (!token) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as any;
|
||||
|
||||
// Only superadmin or admin can execute Live Kill
|
||||
if (decoded.role !== 'superadmin' && decoded.role !== 'admin') {
|
||||
return NextResponse.json({ error: 'Quantum Jurisdiction Denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const { targetType, targetId, reason } = await req.json();
|
||||
|
||||
if (!targetType || !targetId) {
|
||||
return NextResponse.json({ error: 'Target not specified' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Insert into Live Kill Registry using writerDb (Alpha Node)
|
||||
await writerDb.insert(liveKillSwitches).values({
|
||||
tenantId: decoded.tenantId,
|
||||
targetType,
|
||||
targetId,
|
||||
reason: reason || 'Violation of Quantum Directives',
|
||||
issuedBy: decoded.email,
|
||||
});
|
||||
|
||||
// Log the execution
|
||||
await writerDb.insert(quantumLogs).values({
|
||||
tenantId: decoded.tenantId,
|
||||
actor: decoded.email,
|
||||
action: 'LIVE_KILL_EXECUTION',
|
||||
targetId: `${targetType}:${targetId}`,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || '127.0.0.1',
|
||||
userAgent: req.headers.get('user-agent'),
|
||||
});
|
||||
|
||||
// PANOPTICON → XCU RPC Bridge: Kirim sinyal KILL ke mesin Rust
|
||||
// XCU akan memutus QUIC stream secara fisik dalam <0.05ms
|
||||
const XCU_RPC_PORT = parseInt(process.env.XCU_RPC_PORT || '9090');
|
||||
try {
|
||||
const net = await import('net');
|
||||
const rpcPayload = JSON.stringify({
|
||||
action: 'KILL_SESSION',
|
||||
target_id: targetId,
|
||||
reason: reason || 'Otoritas Puncak',
|
||||
issued_by: decoded.email,
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const client = new net.Socket();
|
||||
client.connect(XCU_RPC_PORT, '127.0.0.1', () => {
|
||||
client.write(rpcPayload);
|
||||
client.on('data', () => { client.destroy(); resolve(); });
|
||||
setTimeout(() => { client.destroy(); resolve(); }, 2000);
|
||||
});
|
||||
client.on('error', (err: Error) => {
|
||||
console.warn('[XCU RPC] Engine tidak tersedia, kill hanya tercatat di DB:', err.message);
|
||||
resolve(); // Non-blocking: kill tetap berhasil di DB meskipun engine offline
|
||||
});
|
||||
});
|
||||
} catch (rpcErr: any) {
|
||||
console.warn('[XCU RPC] Bridge error (non-fatal):', rpcErr.message);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Target Successfully Terminated via PANOPTICON + XCU RPC' });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[LIVE KILL ERROR]', error);
|
||||
return NextResponse.json({ error: 'Execution Failed' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
// Konfigurasi CORS agar JUMPA.ID VC (atau subdomain lain) bisa mengakses endpoint ini
|
||||
function applyCorsHeaders(res: NextResponse, req: Request) {
|
||||
const origin = req.headers.get('origin');
|
||||
// Di sistem produksi, pastikan hanya mengizinkan origin yang valid (misal: *.ultramodul.xyz)
|
||||
if (origin && (origin.includes('ultramodul.xyz') || origin.includes('localhost'))) {
|
||||
res.headers.set('Access-Control-Allow-Origin', origin);
|
||||
}
|
||||
res.headers.set('Access-Control-Allow-Credentials', 'true');
|
||||
res.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||
res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function OPTIONS(req: Request) {
|
||||
const res = new NextResponse(null, { status: 204 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
// 1. Ekstrak Session Token JUMPA.ID (Authentication)
|
||||
const cookieHeader = req.headers.get('cookie') || '';
|
||||
const cookies = Object.fromEntries(cookieHeader.split('; ').map(c => c.split('=')));
|
||||
const sessionToken = cookies['jumpa_token'];
|
||||
|
||||
if (!sessionToken) {
|
||||
const res = NextResponse.json({ error: 'Unauthorized: No session token found' }, { status: 401 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
// 2. Dekripsi Session Token
|
||||
const jwtSecret = process.env.JWT_SECRET;
|
||||
if (!jwtSecret) {
|
||||
throw new Error('JWT_SECRET is not configured');
|
||||
}
|
||||
|
||||
let decodedSession: { licenses?: Record<string, string>; tenantName?: string };
|
||||
try {
|
||||
decodedSession = jwt.verify(sessionToken, jwtSecret) as { licenses?: Record<string, string>; tenantName?: string };
|
||||
} catch (_e) {
|
||||
const res = NextResponse.json({ error: 'Unauthorized: Invalid session token' }, { status: 401 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
|
||||
// 3. Ekstrak hak akses (licenses) dari Token Session
|
||||
// Format licenses: { "chat": "GRANTED", "vc": "GRANTED", "recording": "GRANTED", "pulsar_codec": "GRANTED" }
|
||||
const licenses = decodedSession.licenses || {};
|
||||
const tenantName = decodedSession.tenantName || 'UNKNOWN_TENANT';
|
||||
|
||||
// 4. Translasi fitur IAM ke Module ID XCU Core (Ala Carte Modules)
|
||||
const allowedModules: number[] = [];
|
||||
|
||||
// Pengecualian Khusus VIP TELAH DIHAPUS (No more hardcoded bypass)
|
||||
|
||||
// Auto-Pilot & JVC Package Logic
|
||||
if (licenses['pulsar_codec'] === 'GRANTED' || licenses['recording'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(43); // Modul 43: PulsarCodec / Recording / Hot-Swap HD
|
||||
}
|
||||
if (licenses['crdt_chat'] === 'GRANTED' || licenses['x_ray_log'] === 'GRANTED' || licenses['JC'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(72); // Modul 72: Neural CRDT Chat / X-Ray Diagnostic
|
||||
}
|
||||
if (licenses['ebpf_shield'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(88); // Modul 88: The eBPF Shield (Ring-0 DDoS)
|
||||
}
|
||||
if (licenses['ouroboros_sla'] === 'GRANTED' || licenses['JVC'] === 'GRANTED') {
|
||||
allowedModules.push(99); // Modul 99: Ouroboros Automation (Self-Healing SLA)
|
||||
}
|
||||
|
||||
// 5. Generate The Quantum Entitlement Token (JWS murni)
|
||||
// Ditandatangani menggunakan kunci simetris yang hanya diketahui oleh IAM dan XCU Core
|
||||
const quantumSecret = process.env.XCU_TOKEN_SECRET || "UltR4S3cr3T_XCU_Key_2026!"; // Sinkron dengan Rust xcu-core
|
||||
|
||||
const quantumToken = jwt.sign(
|
||||
{
|
||||
tenant: tenantName,
|
||||
allowed_modules: allowedModules,
|
||||
},
|
||||
quantumSecret,
|
||||
{ expiresIn: '8h' }
|
||||
);
|
||||
|
||||
const res = NextResponse.json({
|
||||
success: true,
|
||||
token: quantumToken,
|
||||
modules: allowedModules,
|
||||
}, { status: 200 });
|
||||
|
||||
return applyCorsHeaders(res, req);
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error('[QUANTUM TOKEN ERROR]', error);
|
||||
const res = NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
return applyCorsHeaders(res, req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
interface IAMContextType {
|
||||
isClientRole: boolean;
|
||||
permissions: Record<string, string[]> | null;
|
||||
defaultClientPermissions: Record<string, string[]>;
|
||||
canSee: (modName: string) => boolean;
|
||||
}
|
||||
|
||||
export const IAMContext = createContext<IAMContextType | null>(null);
|
||||
|
||||
export const useIAM = () => {
|
||||
const context = useContext(IAMContext);
|
||||
if (!context) {
|
||||
throw new Error("useIAM must be used within an IAMProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
import { useOmni } from "@/components/OmniSyncProvider";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useDictionary();
|
||||
const { theme, setTheme, currency, setCurrency, locale, setLocale } = useOmni();
|
||||
const [email, setEmail] = useState("");
|
||||
const [role, setRole] = useState("user");
|
||||
const [licenses, setLicenses] = useState<Record<string, string>>({});
|
||||
const [, setEngineStrategy] = useState("XCU_GLOBAL_MESH");
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch("/api/auth/me");
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
setEmail(data.email);
|
||||
setRole(data.role);
|
||||
setLicenses(data.licenses || {});
|
||||
if (data.mediaEngineStrategy) {
|
||||
setEngineStrategy(data.mediaEngineStrategy);
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/api/auth/logout', { method: 'POST' });
|
||||
window.location.href = '/';
|
||||
} catch (e) {
|
||||
console.error("Logout failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] text-white selection:bg-brand/30 selection:text-white relative overflow-x-hidden pb-24 md:pb-6">
|
||||
{/* Dynamic Background */}
|
||||
<div className="fixed inset-0 z-0 pointer-events-none">
|
||||
<div className="absolute top-[-10%] left-[-10%] w-[50%] h-[50%] bg-brand/10 rounded-full blur-[120px] animate-pulse"></div>
|
||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/10 rounded-full blur-[100px]" style={{animationDelay: '2s'}}></div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 md:px-6 pt-6 md:pt-10 relative z-10">
|
||||
|
||||
{/* Top Header - Ultra Refined */}
|
||||
<header className="flex justify-between items-center glass-panel p-4 md:p-5 rounded-2xl mb-8 border-white/5 shadow-2xl">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative group">
|
||||
<div className="absolute -inset-1 bg-linear-to-r from-brand to-blue-600 rounded-full blur opacity-40 group-hover:opacity-75 transition duration-1000 group-hover:duration-200"></div>
|
||||
<div className="relative w-12 h-12 md:w-14 md:h-14 rounded-full bg-black flex items-center justify-center text-white font-black text-lg border border-white/10">
|
||||
{email ? email.substring(0, 2).toUpperCase() : "US"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg md:text-xl font-black tracking-tighter uppercase text-white/90">
|
||||
JUMPA.ID <span className="text-brand">ULTRA</span>
|
||||
</h1>
|
||||
<p className="text-[10px] md:text-xs text-gray-500 font-mono uppercase tracking-widest">
|
||||
{role} • node-alpha-synchronized
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* SETTINGS MATRIX - DUAL BHS / THEME / CURRENCY */}
|
||||
<div className="hidden lg:flex items-center gap-2 p-1.5 glass-panel rounded-xl border-white/5 mr-2">
|
||||
<button onClick={() => setLocale(locale === 'id' ? 'en' : 'id')} className="px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-[10px] font-black uppercase tracking-widest border border-white/5 transition-all">
|
||||
{locale === 'id' ? 'ID 🇮🇩' : 'EN 🇺🇸'}
|
||||
</button>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/5 transition-all text-amber-500">
|
||||
{theme === 'dark' ? (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"></path></svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
)}
|
||||
</button>
|
||||
<select value={currency} onChange={(e) => setCurrency(e.target.value as any)} className="bg-transparent text-[10px] font-black uppercase tracking-widest border-none focus:ring-0 cursor-pointer text-emerald-500">
|
||||
<option value="Rp" className="bg-[#050b14]">IDR</option>
|
||||
<option value="USD" className="bg-[#050b14]">USD</option>
|
||||
<option value="Crypto" className="bg-[#050b14]">XCU</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex items-center gap-3 bg-black/40 border border-white/5 rounded-xl px-4 py-2 mr-4">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[9px] text-gray-500 uppercase font-bold">{t('Dashboard.mesh_sync')}</span>
|
||||
<span className="text-[10px] text-brand font-mono">3/3 NODES READY</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1.5 h-4 bg-brand rounded-full animate-pulse"></div>
|
||||
<div className="w-1.5 h-4 bg-brand/60 rounded-full animate-pulse" style={{animationDelay: '0.2s'}}></div>
|
||||
<div className="w-1.5 h-4 bg-brand/30 rounded-full animate-pulse" style={{animationDelay: '0.4s'}}></div>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="p-3 bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/20 rounded-xl transition-all">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section - Mobile First */}
|
||||
<section className="mb-10 text-center md:text-left">
|
||||
<h2 className="text-3xl md:text-5xl font-black tracking-tight mb-2">{t('Dashboard.welcome')}, <span className="text-transparent bg-clip-text bg-linear-to-r from-white to-gray-500">{email.split('@')[0]}</span></h2>
|
||||
<p className="text-gray-400 text-sm md:text-base max-w-2xl">{t('Index.subtitle')}. Cepat, Aman, dan Tersinkronisasi secara Kuantum.</p>
|
||||
</section>
|
||||
|
||||
{/* Main Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
{/* Chat Pilar */}
|
||||
{licenses['chat'] !== 'HIDDEN' && (
|
||||
<button
|
||||
onClick={() => router.push('/c')}
|
||||
className="group relative p-6 md:p-10 glass-panel rounded-3xl border-white/5 hover:border-emerald-500/30 transition-all duration-500 text-left overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full blur-3xl group-hover:bg-emerald-500/20 transition-all"></div>
|
||||
<div className="flex items-center gap-6 mb-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-emerald-500/10 flex items-center justify-center text-emerald-500 group-hover:scale-110 transition-transform">
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M19.005 3.175H4.674C3.642 3.175 3 3.789 3 4.821V21.02l3.544-3.514h12.461c1.033 0 2.064-1.06 2.064-2.093V4.821c-.001-1.032-1.032-1.646-2.064-1.646zm-4.989 9.869H7.041V11.1h6.975v1.944zm3-4H7.041V7.1h9.975v1.944z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl md:text-2xl font-black text-white group-hover:text-emerald-400 transition-colors">JUMPA Chat</h3>
|
||||
<span className="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-0.5 rounded-full font-mono uppercase">E2EE Secured</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">Komunikasi teks dan kolaborasi tim dengan enkripsi tingkat militer dan fitur Chameleon Mirage.</p>
|
||||
<div className="mt-6 flex items-center text-emerald-500 font-bold text-xs tracking-widest uppercase gap-2 group-hover:translate-x-2 transition-transform">
|
||||
Luncurkan Obrolan <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* VC Pilar */}
|
||||
{licenses['vc'] !== 'HIDDEN' && (
|
||||
<button
|
||||
onClick={() => router.push('/vc')}
|
||||
className="group relative p-6 md:p-10 glass-panel rounded-3xl border-white/5 hover:border-red-500/30 transition-all duration-500 text-left overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-red-500/10 rounded-full blur-3xl group-hover:bg-red-500/20 transition-all"></div>
|
||||
<div className="flex items-center gap-6 mb-6">
|
||||
<div className="w-16 h-16 rounded-2xl bg-red-500/10 flex items-center justify-center text-red-500 group-hover:scale-110 transition-transform">
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl md:text-2xl font-black text-white group-hover:text-red-400 transition-colors">JUMPA Meet</h3>
|
||||
<span className="text-[10px] bg-red-500/20 text-red-400 px-2 py-0.5 rounded-full font-mono uppercase">XCU Engine v2</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm leading-relaxed">Video conference definisi tinggi dengan latensi ultra-rendah dan integrasi eBPF kernel bypass.</p>
|
||||
<div className="mt-6 flex items-center text-red-500 font-bold text-xs tracking-widest uppercase gap-2 group-hover:translate-x-2 transition-transform">
|
||||
Mulai Rapat <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Admin Section if authorized */}
|
||||
{(role === 'admin' || role === 'superadmin') && (
|
||||
<div className="mt-8">
|
||||
<button
|
||||
onClick={() => router.push('/admin')}
|
||||
className="w-full p-6 glass-panel rounded-2xl border-white/5 border-dashed hover:border-amber-500/30 transition-all flex items-center justify-between group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center text-amber-500">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h4 className="font-bold text-white group-hover:text-amber-400 transition-colors">Admin Management Center</h4>
|
||||
<p className="text-[10px] text-gray-500 uppercase tracking-widest">Atur Pengguna, Lisensi, dan Matrix Modul</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-gray-500 group-hover:text-amber-500 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Mobile Bottom Navigation - 100% Mobile Compatible */}
|
||||
<nav className="fixed bottom-4 left-4 right-4 md:hidden glass-panel rounded-2xl p-2 z-50 border-white/10 shadow-[0_-10px_40px_rgba(0,0,0,0.5)] flex justify-around items-center">
|
||||
<button onClick={() => router.push('/dashboard')} className="flex flex-col items-center gap-1 p-2 text-brand">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Home</span>
|
||||
</button>
|
||||
<button onClick={() => router.push('/c')} className="flex flex-col items-center gap-1 p-2 text-gray-400">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Chat</span>
|
||||
</button>
|
||||
<button onClick={() => router.push('/vc')} className="flex flex-col items-center gap-1 p-2 text-gray-400">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Meet</span>
|
||||
</button>
|
||||
<button onClick={() => router.push('/admin')} className="flex flex-col items-center gap-1 p-2 text-gray-400">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
|
||||
<span className="text-[9px] font-bold uppercase">Profile</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* Ultra Footer */}
|
||||
<footer className="text-center py-10 opacity-30">
|
||||
<p className="text-[10px] font-mono tracking-[0.2em] uppercase">Jumpa.ID Ecosystem • Harmonic OS Optimized</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,220 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand: #0b5cff;
|
||||
--color-brand-glow: rgba(11, 92, 255, 0.4);
|
||||
--color-dark-100: #f5f5f5;
|
||||
--color-dark-200: #eaeaea;
|
||||
--color-dark-300: #d4d4d4;
|
||||
|
||||
--font-sans: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #f8fafc; /* Premium Light Gray/Blue */
|
||||
--foreground: #0f172a; /* Slate 900 */
|
||||
--glass-bg: rgba(255, 255, 255, 0.7);
|
||||
--glass-border: rgba(15, 23, 42, 0.1);
|
||||
--panel-bg: #ffffff;
|
||||
--panel-border: rgba(15, 23, 42, 0.05);
|
||||
--color-brand: #0b5cff;
|
||||
--accent-emerald: #10b981;
|
||||
--accent-amber: #f59e0b;
|
||||
--accent-red: #ef4444;
|
||||
--safe-top: env(safe-area-inset-top);
|
||||
--safe-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: #050b14; /* Deep space navy */
|
||||
--foreground: #f8fafc;
|
||||
--glass-bg: rgba(10, 16, 29, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--panel-bg: #111b21;
|
||||
--panel-border: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Advanced Quantum Mesh & Ultra-Modern Effects */
|
||||
.quantum-mesh {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background: radial-gradient(circle at 50% 50%, #1e293b 0%, #050b14 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quantum-mesh::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: conic-gradient(
|
||||
from 180deg at 50% 50%,
|
||||
transparent 0deg,
|
||||
var(--color-brand) 90deg,
|
||||
transparent 180deg,
|
||||
var(--accent-emerald) 270deg,
|
||||
transparent 360deg
|
||||
);
|
||||
animation: rotate-mesh 20s linear infinite;
|
||||
opacity: 0.15;
|
||||
filter: blur(80px);
|
||||
}
|
||||
|
||||
@keyframes rotate-mesh {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.05);
|
||||
border-radius: 24px;
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-5px);
|
||||
box-shadow:
|
||||
0 35px 60px -15px rgba(0, 0, 0, 0.6),
|
||||
0 0 30px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.input-ambient {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-ambient:focus {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--color-brand);
|
||||
box-shadow: 0 0 20px rgba(11, 92, 255, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.btn-ultra {
|
||||
background: linear-gradient(135deg, var(--color-brand) 0%, #00d2ff 100%);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-ultra::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transform: rotate(45deg);
|
||||
transition: 0.5s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-ultra:hover::after {
|
||||
left: 120%;
|
||||
}
|
||||
|
||||
.btn-ultra:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 10px 25px rgba(11, 92, 255, 0.4);
|
||||
}
|
||||
|
||||
/* HarmonyOS, Samsung, Safari & Global Browser Parity Optimization */
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
overflow-x: hidden;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scroll-behavior: smooth;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: var(--safe-top) 0 var(--safe-bottom) 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
touch-action: manipulation;
|
||||
overscroll-behavior-y: none; /* Prevents pull-to-refresh quirks in Chrome/Samsung */
|
||||
}
|
||||
|
||||
/* Touch-Optimized Interactions */
|
||||
button, a, input, select, textarea {
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], input[type="email"] {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.text-gradient {
|
||||
background: linear-gradient(to right, #ffffff, #94a3b8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for Premium Feel - Cross Browser */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 10px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
/* Firefox Scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
||||
}
|
||||
|
||||
/* Android/Huawei/Samsung Input Reset */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: white !important;
|
||||
-webkit-box-shadow: 0 0 0px 1000px #050b14 inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
/* Safari Specific Fixes */
|
||||
@supports (-webkit-hyphens:none) {
|
||||
.glass-card {
|
||||
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Huawei Browser Optimization */
|
||||
@media screen and (max-width: 480px) {
|
||||
.glass-card {
|
||||
border-radius: 16px; /* Slightly sharper for smaller mobile screens */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import "./globals.css";
|
||||
import crypto from "crypto";
|
||||
|
||||
import type { Viewport } from "next";
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#00ff88",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "JUMPA.ID | Enterprise Gateway",
|
||||
description: "Secure SaaS B2B Ecosystem",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
statusBarStyle: "black-translucent",
|
||||
title: "JUMPA.ID",
|
||||
},
|
||||
};
|
||||
|
||||
import { OmniSyncProvider } from "@/components/OmniSyncProvider";
|
||||
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const cookieStore = await cookies();
|
||||
const locale = cookieStore.get('NEXT_LOCALE')?.value || 'id';
|
||||
|
||||
// TSM Versioning Format: [TSM.ID].hh.mm.ss.DD.MM.YYYY.XXXX
|
||||
const date = new Date();
|
||||
const format2 = (n: number) => n.toString().padStart(2, '0');
|
||||
const hh = format2(date.getUTCHours());
|
||||
const mm = format2(date.getUTCMinutes());
|
||||
const ss = format2(date.getUTCSeconds());
|
||||
const DD = format2(date.getUTCDate());
|
||||
const MM = format2(date.getUTCMonth() + 1);
|
||||
const YYYY = date.getUTCFullYear();
|
||||
const XXXX = crypto.randomBytes(2).toString('hex').toUpperCase();
|
||||
const tsmVersion = `[TSM.ID].${hh}.${mm}.${ss}.${DD}.${MM}.${YYYY}.${XXXX}`;
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<link rel="apple-touch-icon" href="/icon512_maskable.png" />
|
||||
<script dangerouslySetInnerHTML={{__html: `
|
||||
// PKX NUCLEAR CACHE BUSTER: Unregister all Service Workers & clear all caches
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
for (var i = 0; i < registrations.length; i++) {
|
||||
registrations[i].unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
if ('caches' in window) {
|
||||
caches.keys().then(function(names) {
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
caches.delete(names[i]);
|
||||
}
|
||||
});
|
||||
}
|
||||
`}} />
|
||||
</head>
|
||||
<body className={`antialiased font-sans bg-[#050B14] text-white`}>
|
||||
<OmniSyncProvider initialLocale={locale as any}>
|
||||
{children}
|
||||
{/* TSM PERMANENT WATERMARK */}
|
||||
<div className="fixed bottom-1 right-1 z-[9999] opacity-30 pointer-events-none select-none">
|
||||
<span className="text-[8px] font-mono text-white tracking-widest drop-shadow-[0_0_5px_rgba(255,255,255,0.8)]">
|
||||
{tsmVersion}
|
||||
</span>
|
||||
</div>
|
||||
</OmniSyncProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export default function OmniPayGateway() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [status, setStatus] = useState<"pending" | "scanning" | "success" | "failed">("pending");
|
||||
|
||||
const amount = searchParams.get("amount") || "0";
|
||||
const licenses = searchParams.get("licenses") || "";
|
||||
const sessionId = searchParams.get("session_id") || "";
|
||||
|
||||
// Animasi Inisialisasi
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (status === "pending") setStatus("scanning");
|
||||
}, 1500);
|
||||
}, [status]);
|
||||
|
||||
const handlePay = () => {
|
||||
setIsProcessing(true);
|
||||
setStatus("scanning");
|
||||
|
||||
// Simulasi X-RAY Vision AI validasi pembayaran
|
||||
setTimeout(() => {
|
||||
setStatus("success");
|
||||
setTimeout(() => {
|
||||
router.push(`/id/admin?payment=success&session_id=${sessionId}`);
|
||||
}, 2000);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push(`/id/admin?payment=cancel`);
|
||||
};
|
||||
|
||||
const formattedAmount = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(Number(amount));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050B14] flex items-center justify-center p-4 relative overflow-hidden font-sans">
|
||||
{/* Background Kuantum */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-[#050B14] to-[#050B14]"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-[#00ff88]/5 pointer-events-none"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-md bg-black/60 backdrop-blur-2xl border border-blue-500/30 rounded-3xl p-8 shadow-[0_0_50px_rgba(59,130,246,0.15)]">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-900 mb-4 shadow-[0_0_30px_rgba(37,99,235,0.5)]">
|
||||
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<h1 className="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-cyan-300 tracking-tight">
|
||||
OMNIPAY X
|
||||
</h1>
|
||||
<p className="text-blue-400/60 text-sm mt-1 uppercase tracking-widest font-semibold">Sovereign Financial Vault</p>
|
||||
</div>
|
||||
|
||||
{/* Invoice Card */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-2xl p-6 mb-8 relative overflow-hidden group hover:border-blue-500/50 transition-colors duration-500">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-3xl group-hover:bg-blue-500/20 transition-all duration-500"></div>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-gray-400 text-sm">Sesi Otorisasi</span>
|
||||
<span className="text-xs font-mono text-blue-400 bg-blue-500/10 px-2 py-1 rounded border border-blue-500/20">
|
||||
{sessionId.substring(0, 12).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-gray-500 text-xs uppercase tracking-wider mb-1">Upgrade Infrastruktur</p>
|
||||
<p className="text-white font-medium text-lg">{licenses.split(',').join(' & ')}</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-white/10 flex justify-between items-end">
|
||||
<span className="text-gray-400 text-sm">Total Tagihan</span>
|
||||
<span className="text-3xl font-bold text-white tracking-tight">{formattedAmount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Engine */}
|
||||
{status === "success" ? (
|
||||
<div className="text-center p-6 bg-green-500/10 border border-green-500/30 rounded-2xl">
|
||||
<div className="w-12 h-12 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-3">
|
||||
<svg className="w-6 h-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-green-400 font-bold text-lg mb-1">Otorisasi Kuantum Berhasil</h3>
|
||||
<p className="text-green-400/70 text-sm">Lisensi sedang disuntikkan ke simpul Alpha...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={handlePay}
|
||||
disabled={isProcessing}
|
||||
className={`w-full py-4 rounded-xl font-bold text-white shadow-lg transition-all duration-300 relative overflow-hidden ${
|
||||
isProcessing
|
||||
? "bg-blue-900/50 cursor-not-allowed border border-blue-500/30"
|
||||
: "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 border border-transparent shadow-blue-500/20 hover:shadow-blue-500/40 transform hover:-translate-y-0.5"
|
||||
}`}
|
||||
>
|
||||
{isProcessing ? (
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<svg className="animate-spin h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="tracking-wide">Memproses Transmisi Kriptografis...</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="tracking-wider">OTORISASI PEMBAYARAN</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={isProcessing}
|
||||
className="w-full py-3 rounded-xl font-medium text-gray-400 hover:text-white hover:bg-white/5 transition-colors text-sm tracking-wide"
|
||||
>
|
||||
Batalkan Sesi
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center flex items-center justify-center gap-2 text-xs text-gray-600">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span className="uppercase tracking-widest">Diamankan oleh JUMPA.ID Zero-Knowledge Engine</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
import { io, Socket } from "@/lib/zero-socket";
|
||||
import { useOmni } from "@/components/OmniSyncProvider";
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useDictionary();
|
||||
const { theme, setTheme, locale, setLocale } = useOmni();
|
||||
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
const [loginMode, setLoginMode] = useState<'email' | 'qr'>('email');
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [successMsg, setSuccessMsg] = useState("");
|
||||
const [qrScanned, setQrScanned] = useState(false);
|
||||
const [qrSessionId, setQrSessionId] = useState("");
|
||||
|
||||
// 1. Quantum QR Sync
|
||||
useEffect(() => {
|
||||
if (loginMode === 'qr' && !qrSessionId) {
|
||||
const sessionId = (crypto.getRandomValues(new Uint32Array(1))[0] % 900000 + 100000).toString();
|
||||
setQrSessionId(sessionId);
|
||||
|
||||
const socket: Socket = io(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("qr_auth_init", { sessionId });
|
||||
});
|
||||
|
||||
socket.on("qr_auth_approved", async (data: unknown) => {
|
||||
const payload = data as { signature: string; email: string };
|
||||
setQrScanned(true);
|
||||
setSuccessMsg("Quantum Sync Approved! Verifying Handshake...");
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/qr-auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId: sessionId,
|
||||
signature: payload.signature,
|
||||
email: payload.email
|
||||
})
|
||||
});
|
||||
const verifyData = await res.json();
|
||||
|
||||
if (verifyData.token) {
|
||||
setSuccessMsg("Handshake Valid! Accessing Multiverse...");
|
||||
setTimeout(() => {
|
||||
window.location.href = `/${locale}/dashboard?auth_token=${verifyData.token}`;
|
||||
}, 1000);
|
||||
} else {
|
||||
setErrorMsg("Verification Failed: Invalid Quantum Signature.");
|
||||
setQrScanned(false);
|
||||
}
|
||||
} catch (_e) {
|
||||
setErrorMsg("Connection to Crypto Vault Interrupted.");
|
||||
setQrScanned(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => { socket.disconnect(); };
|
||||
}
|
||||
}, [loginMode, qrSessionId, locale]);
|
||||
|
||||
// 2. Auth Actions
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setErrorMsg("");
|
||||
setSuccessMsg("");
|
||||
|
||||
const endpoint = isRegister ? '/api/auth/register' : '/api/auth/login';
|
||||
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
setErrorMsg(data.error || 'Authentication Failed');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSuccessMsg(isRegister ? "Registration Successful! Constructing Identity..." : "Identity Verified! Syncing Neural Path...");
|
||||
|
||||
setTimeout(() => {
|
||||
if (data.user && data.user.role === 'superadmin') {
|
||||
window.location.href = '/supreme-admin';
|
||||
} else if (data.user && data.user.role === 'admin') {
|
||||
window.location.href = '/admin';
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}, 800);
|
||||
|
||||
} catch (_err) {
|
||||
setErrorMsg('Quantum Tunnel to Database Lost.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative overflow-hidden flex flex-col items-center justify-center p-6 selection:bg-emerald-500/30">
|
||||
|
||||
{/* 🌌 QUANTUM MESH BACKGROUND (Innovative Technology) */}
|
||||
<div className="quantum-mesh" />
|
||||
|
||||
{/* Header Bar - Zoom Style Minimalist */}
|
||||
<header className="fixed top-0 w-full h-20 px-8 flex items-center justify-between z-50 bg-black/10 backdrop-blur-md border-b border-white/5">
|
||||
<div className="flex items-center gap-3 group cursor-pointer">
|
||||
<div className="w-10 h-10 bg-emerald-600 rounded-xl flex items-center justify-center shadow-[0_0_20px_rgba(16,185,129,0.4)] group-hover:scale-110 transition-transform">
|
||||
<svg className="w-6 h-6 text-black" 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>
|
||||
</div>
|
||||
<span className="text-xl font-black tracking-tighter text-white uppercase italic">JUMPA.ID <span className="text-emerald-500 not-italic">MESH</span></span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Multi-Language Switcher */}
|
||||
<div className="flex bg-white/5 p-1 rounded-xl border border-white/10">
|
||||
<button onClick={() => setLocale('id')} className={`px-3 py-1 text-[10px] font-black rounded-lg transition-all ${locale === 'id' ? 'bg-emerald-600 text-black shadow-lg' : 'text-white/40 hover:text-white'}`}>ID</button>
|
||||
<button onClick={() => setLocale('en')} className={`px-3 py-1 text-[10px] font-black rounded-lg transition-all ${locale === 'en' ? 'bg-emerald-600 text-black shadow-lg' : 'text-white/40 hover:text-white'}`}>EN</button>
|
||||
</div>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="text-white/40 hover:text-white transition-colors">
|
||||
{theme === 'dark' ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd"></path></svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 🛡️ MAIN LOGIN CARD - SPECTACULAR WEDDING OF DESIGN */}
|
||||
<div className="relative z-10 w-full max-w-[440px] animate-in fade-in slide-in-from-bottom-8 duration-700">
|
||||
|
||||
{/* Top Status Indicators (WhatsApp Style) */}
|
||||
<div className="flex justify-center mb-8">
|
||||
<div className="bg-emerald-950/30 border border-emerald-500/20 backdrop-blur-xl px-4 py-1.5 rounded-full flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_10px_#10b981]"></div>
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-emerald-500">Quantum Vault Synchronized</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-10">
|
||||
<div className="text-center mb-10">
|
||||
<h2 className="text-4xl font-black text-white tracking-tighter mb-2">
|
||||
{isRegister ? t("Index.initialize_account") : t("Index.title")}
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm font-medium">{t("Index.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
{!isRegister && (
|
||||
<div className="flex bg-white/5 p-1 rounded-2xl border border-white/10 mb-8">
|
||||
<button
|
||||
onClick={() => setLoginMode('email')}
|
||||
className={`flex-1 py-3 text-xs font-black uppercase tracking-widest rounded-xl transition-all ${loginMode === 'email' ? 'bg-white text-black shadow-xl scale-100' : 'text-slate-500 hover:text-white'}`}
|
||||
>
|
||||
{t("Index.credentials")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLoginMode('qr')}
|
||||
className={`flex-1 py-3 text-xs font-black uppercase tracking-widest rounded-xl transition-all flex items-center justify-center gap-2 ${loginMode === 'qr' ? 'bg-white text-black shadow-xl scale-100' : 'text-slate-500 hover:text-white'}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm14 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z"></path></svg>
|
||||
{t("Index.quantum_qr")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loginMode === 'email' || isRegister ? (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{errorMsg && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 p-4 rounded-2xl text-xs text-center font-bold uppercase tracking-widest animate-shake">
|
||||
{errorMsg}
|
||||
</div>
|
||||
)}
|
||||
{successMsg && (
|
||||
<div className="bg-emerald-500/10 border border-emerald-500/20 text-emerald-500 p-4 rounded-2xl text-xs text-center font-bold uppercase tracking-widest animate-pulse">
|
||||
{successMsg}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="group relative">
|
||||
<input
|
||||
type="email" required
|
||||
className="w-full input-ambient rounded-2xl px-5 py-4 text-[15px] placeholder-slate-500 font-medium"
|
||||
placeholder={t("Index.email_placeholder")}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<div className="absolute right-5 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-emerald-500 transition-colors">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group relative">
|
||||
<input
|
||||
type="password" required minLength={6}
|
||||
className="w-full input-ambient rounded-2xl px-5 py-4 text-[15px] placeholder-slate-500 font-medium"
|
||||
placeholder={t("Index.password_placeholder")}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<div className="absolute right-5 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-emerald-500 transition-colors">
|
||||
<svg className="w-5 h-5" 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isRegister && (
|
||||
<div className="flex items-center justify-between text-[11px] px-1 font-black uppercase tracking-widest">
|
||||
<label className="flex items-center text-slate-500 gap-3 cursor-pointer hover:text-white transition-colors">
|
||||
<input type="checkbox" className="rounded-lg border-white/10 bg-white/5 text-emerald-500 focus:ring-emerald-500 w-5 h-5 transition-all" />
|
||||
<span>{t("Index.persistent_session")}</span>
|
||||
</label>
|
||||
<a href="#" className="text-emerald-500 hover:text-white transition-colors">{t("Index.reset_key")}</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full btn-ultra rounded-2xl py-5 flex items-center justify-center gap-3 ${loading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="w-6 h-6 border-4 border-black/20 border-t-black rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<>
|
||||
<span>{isRegister ? t("Index.initialize_account") : t("Index.enter_multiverse")}</span>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-4 animate-in fade-in zoom-in duration-500">
|
||||
<div className="relative w-56 h-56 p-4 rounded-[40px] bg-white/5 border border-white/10 mb-8 flex items-center justify-center overflow-hidden group">
|
||||
{/* SCANNING LASER EFFECT */}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-emerald-500 shadow-[0_0_20px_#10b981] animate-[scan_3s_linear_infinite] z-20"></div>
|
||||
|
||||
<div className="w-full h-full rounded-[30px] bg-black/40 flex flex-col items-center justify-center relative z-10 border border-white/5">
|
||||
{!qrScanned ? (
|
||||
<>
|
||||
<div className="text-[10px] text-emerald-500 mb-2 font-black tracking-[0.4em] uppercase">QUANTUM SESSION</div>
|
||||
<div className="text-3xl text-white font-black tracking-[0.2em]">{qrSessionId || '••••••'}</div>
|
||||
<div className="mt-4 flex gap-1">
|
||||
{[...Array(6)].map((_,i) => <div key={i} className="w-1.5 h-1.5 bg-emerald-500/20 rounded-full animate-pulse" style={{animationDelay: `${i*0.2}s`}}></div>)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center text-emerald-500 animate-in zoom-in scale-110">
|
||||
<svg className="w-20 h-20 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span className="font-black tracking-[0.5em] text-xs uppercase">CONNECTED</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-black text-white mb-2 tracking-tighter">{t("Index.instant_sync")}</h3>
|
||||
<p className="text-slate-500 text-[13px] text-center max-w-[280px] leading-relaxed font-medium">
|
||||
{t("Index.scan_instructions")}
|
||||
</p>
|
||||
{qrScanned && <p className="text-emerald-500 font-black text-[10px] mt-6 animate-pulse uppercase tracking-[0.3em]">Neural Handshake in Progress...</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-12 pt-8 border-t border-white/5 text-center text-[11px] font-black uppercase tracking-widest">
|
||||
<span className="text-slate-600">
|
||||
{isRegister ? t("Index.known_identity") : t("Index.new_to_multiverse")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setIsRegister(!isRegister); setErrorMsg(""); setSuccessMsg(""); setLoginMode('email'); }}
|
||||
className="ml-3 text-emerald-500 hover:text-white transition-all hover:scale-105"
|
||||
>
|
||||
{isRegister ? t("Index.sign_in") : t("Index.access_now")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="mt-12 text-center text-[10px] text-slate-600 font-black uppercase tracking-[0.4em] space-y-4">
|
||||
<p className="hover:text-slate-400 transition-colors">Enterprise Quantum Mesh Node #Alpha</p>
|
||||
<div className="flex justify-center gap-8 text-slate-700">
|
||||
<a href="#" className="hover:text-emerald-500 transition-colors">Protocol</a>
|
||||
<a href="#" className="hover:text-emerald-500 transition-colors">Sovereignty</a>
|
||||
<a href="#" className="hover:text-emerald-500 transition-colors">Neural Sync</a>
|
||||
</div>
|
||||
<p className="opacity-30">© 2026 JUMPA.ID | XCom Ultra Engine v2.0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes scan {
|
||||
0%, 100% { top: 0; }
|
||||
50% { top: 100%; }
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
.animate-shake {
|
||||
animation: shake 0.4s ease-in-out;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { io, Socket } from "@/lib/zero-socket";
|
||||
|
||||
export default function QRScannerSimulation() {
|
||||
const [sessionId, setSessionId] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [status, setStatus] = useState("Waiting for Input");
|
||||
|
||||
const handleScan = () => {
|
||||
setStatus("Scanning...");
|
||||
const socket: Socket = io(typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.emit("qr_auth_scan_success", { sessionId, token });
|
||||
setStatus("Success! Session authenticated.");
|
||||
setTimeout(() => {
|
||||
socket.disconnect();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
socket.on("connect_error", () => {
|
||||
setStatus("Error connecting to Chat Server Socket.");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a101d] text-white flex flex-col items-center justify-center p-8">
|
||||
<div className="bg-[#111827] p-8 rounded-2xl border border-gray-800 shadow-2xl max-w-md w-full">
|
||||
<h1 className="text-2xl font-bold mb-4 text-brand">📱 QR Scanner Simulation</h1>
|
||||
<p className="text-gray-400 text-sm mb-6">
|
||||
Masukkan Session ID yang muncul di layar IAM Desktop Anda, beserta Token Auth rahasia (contoh JWT) yang dimiliki perangkat mobile ini.
|
||||
</p>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={sessionId}
|
||||
onChange={(e) => setSessionId(e.target.value)}
|
||||
placeholder="Session ID (ex: 12345)"
|
||||
className="w-full bg-black/50 border border-white/10 p-3 rounded-lg mb-4 text-white"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="JWT Token Rahasia dari HP"
|
||||
className="w-full bg-black/50 border border-white/10 p-3 rounded-lg mb-6 text-white"
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={handleScan}
|
||||
className="w-full py-3 bg-brand text-black font-bold rounded-lg hover:opacity-80 transition-opacity">
|
||||
[SCAN] Otentikasi Jarak Jauh
|
||||
</button>
|
||||
|
||||
<p className="mt-6 text-center text-xs font-mono text-gray-500">Status: {status}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function DokumentasiPage() {
|
||||
const [activeTab, setActiveTab] = useState<'anatomi' | 'roadmap' | 'mindmap'>('anatomi');
|
||||
const [activeNode, setActiveNode] = useState<string | null>(null);
|
||||
|
||||
const roadmapData = [
|
||||
{ id: 'v1', time: 'Masa Lalu (Genesis)', title: 'Monolithic Era', desc: 'Sistem tunggal SSR yang berat dan rentan tumbang. Penuh ketergantungan pada satu database.' },
|
||||
{ id: 'v2', time: 'Q1 2026', title: 'Microservices Split', desc: 'Pemecahan paksa tulang punggung. IAM, XCU Ultra, dan Chat WebSocket berdiri di server masing-masing.' },
|
||||
{ id: 'v3', time: 'Q2 2026', title: 'Breakout Multiverse', desc: 'Kemampuan membelah dimensi kamar VC tanpa jeda loading, ditambah Quantum Record untuk privasi absolut.' },
|
||||
{ id: 'v4', time: 'Q3 2026', title: 'Omniversal & Temporal', desc: 'Ekspansi jaringan. 1 Aliran Video ke VPS dibelah menggunakan TEE-Muxer FFmpeg ke YouTube, TikTok, dan Twitter secara simultan. In-Memory Scheduler untuk chat.' },
|
||||
{ id: 'v5', time: 'Masa Depan (Quantum Leap)', title: 'AI Hologram Host', desc: 'Integrasi avatar 3D kecerdasan buatan sebagai moderator otomatis dalam kamar yang menganalisa sentimen emosi partisipan.' }
|
||||
];
|
||||
|
||||
const anatomyData = [
|
||||
{ id: 'otak', icon: '🧠', name: 'Otak (Brain)', tech: 'PostgreSQL & Redis', detail: 'Matriks logika absolut. Mengingat miliaran token JWT dan mencatat setiap detak nadi transaksi penagihan tanpa kesalahan.' },
|
||||
{ id: 'jantung', icon: '🫀', name: 'Jantung (Heart)', tech: 'XCU eBPF Engine', detail: 'Mesin pemompa video. Menerima tetesan data dari peramban dan memancarkannya ke ribuan layar dengan latensi di bawah 50ms.' },
|
||||
{ id: 'syaraf', icon: '⚡', name: 'Syaraf (Nerves)', tech: 'Node.js WebSockets', detail: 'Jaringan listrik real-time. Membawa sinyal chat, peringatan gempa UI, dan komando Temporal Scheduler dalam kecepatan cahaya.' },
|
||||
{ id: 'nadi', icon: '🩸', name: 'Nadi (Pulse)', tech: 'Next.js API', detail: 'Sirkulasi darah. Meneruskan data autentikasi dari Otak ke Kulit dengan pengamanan SSL dan Enkripsi End-to-End.' },
|
||||
{ id: 'tulang', icon: '🦴', name: 'Tulang (Bones)', tech: 'Microservices', detail: 'Kerangka baja. Memastikan jika aplikasi Chat lumpuh, aplikasi Video Conference tetap hidup tanpa gangguan.' },
|
||||
{ id: 'kulit', icon: '🖐️', name: 'Kulit (Skin)', tech: 'Tailwind CSS', detail: 'Sensasi sentuhan antarmuka kelas dunia. Responsif, dinamis, dan diwarnai dengan gradien neon masa depan.' },
|
||||
{ id: 'ruh', icon: '🌌', name: 'Ruh (Soul)', tech: 'Zero-Downtime Philosophy', detail: 'Kesadaran platform bahwa server harus ringan. Mengorbankan browser klien untuk bekerja keras (Client-Side Rendering) demi stabilitas abadi.' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] p-6 md:p-12 text-[#e9edef] font-sans">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between border-b border-gray-800 pb-4">
|
||||
<h1 className="text-3xl font-black text-brand drop-shadow-[0_0_15px_rgba(0,255,136,0.5)]">KITAB SUCI JUMPA.ID</h1>
|
||||
<Link href="/supreme-admin/about" className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg font-bold transition-colors">
|
||||
Kembali ke Version Engine
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* TAB NAVIGATION */}
|
||||
<div className="flex gap-4 border-b border-gray-800 pb-2">
|
||||
<button
|
||||
onClick={() => setActiveTab('anatomi')}
|
||||
className={`px-6 py-3 font-bold rounded-t-xl transition-all ${activeTab === 'anatomi' ? 'bg-brand text-black' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
Anatomi Biologis
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('mindmap')}
|
||||
className={`px-6 py-3 font-bold rounded-t-xl transition-all ${activeTab === 'mindmap' ? 'bg-blue-500 text-white shadow-[0_-5px_20px_rgba(59,130,246,0.3)]' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
Interactive Mindmap
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('roadmap')}
|
||||
className={`px-6 py-3 font-bold rounded-t-xl transition-all ${activeTab === 'roadmap' ? 'bg-purple-500 text-white shadow-[0_-5px_20px_rgba(168,85,247,0.3)]' : 'text-gray-500 hover:text-white'}`}
|
||||
>
|
||||
Ultra Roadmap
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CONTENT AREA */}
|
||||
<div className="bg-[#0b141a] border border-gray-800 rounded-b-2xl rounded-tr-2xl p-8 min-h-[60vh] shadow-2xl relative overflow-hidden">
|
||||
|
||||
{/* TAB 1: ANATOMI */}
|
||||
{activeTab === 'anatomi' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-2xl font-bold text-brand mb-6">Struktur Kehidupan Platform</h2>
|
||||
{anatomyData.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onMouseEnter={() => setActiveNode(item.id)}
|
||||
className={`p-4 rounded-xl border cursor-pointer transition-all duration-300 ${activeNode === item.id ? 'bg-gray-800 border-brand scale-105 z-10 relative shadow-2xl' : 'bg-transparent border-gray-800 hover:border-gray-600'}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-4xl">{item.icon}</span>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{item.name}</h3>
|
||||
<p className="text-sm font-mono text-brand">{item.tech}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-8">
|
||||
{activeNode ? (
|
||||
<div className="bg-[#111b21] border border-brand p-8 rounded-3xl shadow-[0_0_50px_rgba(0,255,136,0.1)] animate-in zoom-in duration-300">
|
||||
<span className="text-7xl block mb-6 text-center">{anatomyData.find(a => a.id === activeNode)?.icon}</span>
|
||||
<h3 className="text-2xl font-bold text-white mb-2 text-center">{anatomyData.find(a => a.id === activeNode)?.name}</h3>
|
||||
<p className="text-center text-brand font-mono text-sm mb-6">{anatomyData.find(a => a.id === activeNode)?.tech}</p>
|
||||
<p className="text-gray-400 text-lg leading-relaxed text-center">
|
||||
{anatomyData.find(a => a.id === activeNode)?.detail}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-600">
|
||||
<svg className="w-24 h-24 mx-auto mb-4 opacity-20" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v2h-2zm0-2h2V7h-2z"/></svg>
|
||||
<p>Arahkan kursor ke salah satu elemen di sebelah kiri untuk melihat detail jiwa.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 2: INTERACTIVE MINDMAP */}
|
||||
{activeTab === 'mindmap' && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500 h-[600px] flex items-center justify-center relative bg-gradient-to-br from-[#050B14] via-[#0A192F] to-[#00ff88]/10">
|
||||
<div className="absolute inset-0 bg-blue-900/5 mix-blend-overlay"></div>
|
||||
|
||||
{/* Custom CSS Mindmap Tree */}
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
{/* Node Pusat */}
|
||||
<div className="bg-blue-600 text-white px-8 py-4 rounded-2xl font-black text-2xl shadow-[0_0_30px_rgba(37,99,235,0.6)] z-20 hover:scale-110 transition-transform cursor-crosshair">
|
||||
JUMPA.ID OMNIVERSE
|
||||
</div>
|
||||
|
||||
{/* Garis Vertikal */}
|
||||
<div className="w-1 h-12 bg-blue-500/50"></div>
|
||||
|
||||
{/* Cabang Horizontal */}
|
||||
<div className="w-[800px] h-1 bg-blue-500/50 flex justify-between relative">
|
||||
<div className="absolute -top-1 -left-1 w-3 h-3 bg-blue-400 rounded-full"></div>
|
||||
<div className="absolute -top-1 left-1/2 -translate-x-1/2 w-3 h-3 bg-blue-400 rounded-full"></div>
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-blue-400 rounded-full"></div>
|
||||
</div>
|
||||
|
||||
{/* 3 Pilar */}
|
||||
<div className="flex w-[800px] justify-between">
|
||||
{/* Pilar 1: IAM */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-1 h-8 bg-blue-500/50"></div>
|
||||
<div className="bg-[#111b21] border-2 border-emerald-500 text-emerald-400 px-6 py-3 rounded-xl font-bold shadow-[0_0_15px_rgba(16,185,129,0.3)] hover:-translate-y-2 transition-transform cursor-pointer">
|
||||
IAM (Identity Access)
|
||||
</div>
|
||||
<div className="w-1 h-8 bg-emerald-500/30"></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Master Owner Dashboard</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Tenant Management</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Ultra Billing Engine</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pilar 2: VC */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-1 h-8 bg-blue-500/50"></div>
|
||||
<div className="bg-[#111b21] border-2 border-red-500 text-red-400 px-6 py-3 rounded-xl font-bold shadow-[0_0_15px_rgba(239,68,68,0.3)] hover:-translate-y-2 transition-transform cursor-pointer">
|
||||
XCU Ultra Engine
|
||||
</div>
|
||||
<div className="w-1 h-8 bg-red-500/30"></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Webinar / Podcast Mode</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Breakout Multiverse</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Quantum Local Record</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pilar 3: CHAT */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="w-1 h-8 bg-blue-500/50"></div>
|
||||
<div className="bg-[#111b21] border-2 border-purple-500 text-purple-400 px-6 py-3 rounded-xl font-bold shadow-[0_0_15px_rgba(168,85,247,0.3)] hover:-translate-y-2 transition-transform cursor-pointer">
|
||||
WebSocket Chat
|
||||
</div>
|
||||
<div className="w-1 h-8 bg-purple-500/30"></div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">Temporal Scheduler</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">TEE-Muxer Omniversal</div>
|
||||
<div className="bg-gray-800 text-gray-300 text-sm px-4 py-2 rounded-lg border border-gray-700">End-to-End Encryption</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TAB 3: ULTRA ROADMAP */}
|
||||
{activeTab === 'roadmap' && (
|
||||
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500 relative py-12">
|
||||
<div className="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-1 bg-linear-to-b from-gray-800 via-purple-500 to-var(--color-brand)"></div>
|
||||
|
||||
<div className="space-y-12">
|
||||
{roadmapData.map((node, index) => (
|
||||
<div key={node.id} className={`flex items-center w-full ${index % 2 === 0 ? 'justify-start' : 'justify-end'} relative`}>
|
||||
|
||||
{/* Lingkaran Garis Waktu */}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 w-6 h-6 rounded-full bg-[#111b21] border-4 border-purple-500 z-10 flex items-center justify-center">
|
||||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<div className={`w-5/12 ${index % 2 === 0 ? 'pr-12 text-right' : 'pl-12 text-left'}`}>
|
||||
<div className="bg-[#111b21] p-6 rounded-2xl border border-gray-700 hover:border-purple-500 transition-colors shadow-xl group">
|
||||
<span className="text-purple-400 font-mono text-sm block mb-2">{node.time}</span>
|
||||
<h3 className="text-2xl font-bold text-white mb-2 group-hover:text-purple-300 transition-colors">{node.title}</h3>
|
||||
<p className="text-gray-400 leading-relaxed text-sm">{node.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function AboutPage() {
|
||||
const [platformName, setPlatformName] = useState("JUMPA.ID");
|
||||
const version = "v4.2.0-ULTRA";
|
||||
|
||||
const handleSave = () => {
|
||||
alert("SYSTEM INJECTION SUCCESS: Platform identity propagated to all nodes.");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] p-6 md:p-12 text-white font-sans selection:bg-amber-500/30">
|
||||
<div className="max-w-5xl mx-auto space-y-12">
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 border-b border-white/5 pb-8">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-purple-600 rounded-lg flex items-center justify-center shadow-[0_0_15px_rgba(168,85,247,0.5)]">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black tracking-tighter uppercase">System Anatomy</h1>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm font-medium">Core engine specifications and multiverse lineage.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/supreme-admin" className="px-5 py-2.5 glass-panel border border-white/10 hover:bg-white/5 rounded-xl font-bold transition-all text-xs uppercase tracking-widest">
|
||||
Dashboard
|
||||
</Link>
|
||||
<span className="px-4 py-2 bg-amber-500/10 text-amber-500 text-[10px] font-black tracking-[0.2em] rounded-full border border-amber-500/20 uppercase">
|
||||
{version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-10">
|
||||
|
||||
{/* LEFT COL: WHITE LABEL */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
<div className="glass-panel p-8 rounded-[2rem] border border-white/10 relative overflow-hidden group">
|
||||
<div className="absolute top-0 left-0 w-2 h-full bg-blue-500"></div>
|
||||
<h2 className="text-xl font-black text-blue-400 mb-6 flex items-center gap-2">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"></path></svg>
|
||||
Identity Morphing (White Label)
|
||||
</h2>
|
||||
<p className="text-gray-400 text-sm mb-8 leading-relaxed">
|
||||
Injeksi identitas global. Mengubah parameter ini akan mendistorsi branding JUMPA.ID di seluruh layer (IAM, XCU, XTM) secara simultan.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-gray-500 text-[10px] font-black mb-3 uppercase tracking-[0.2em]">Master Platform Alias</label>
|
||||
<div className="flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={platformName}
|
||||
onChange={(e) => setPlatformName(e.target.value)}
|
||||
className="flex-1 bg-white/5 text-white px-5 py-4 rounded-2xl border border-white/10 outline-none focus:border-blue-500/50 transition-all font-mono text-sm"
|
||||
placeholder="e.g. ULTRA_MEET"
|
||||
/>
|
||||
<button onClick={handleSave} className="px-8 py-4 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black transition-all shadow-[0_0_20px_rgba(37,99,235,0.3)] active:scale-95 text-xs uppercase tracking-widest">
|
||||
INJECT
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TECH STACK VISUAL */}
|
||||
<div className="glass-panel p-8 rounded-[2rem] border border-white/10 bg-white/[0.02]">
|
||||
<h2 className="text-sm font-black text-gray-500 mb-6 uppercase tracking-[0.2em]">Quantum Stack Integrity</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'ENGINE', val: 'XCU RUST', color: 'text-orange-400' },
|
||||
{ label: 'ORCHESTRA', val: 'NEXT.JS 15', color: 'text-white' },
|
||||
{ label: 'DATABASE', val: 'POSTGRES', color: 'text-blue-400' },
|
||||
{ label: 'TRANSPORT', val: 'QUIC/H3', color: 'text-emerald-400' }
|
||||
].map((t, i) => (
|
||||
<div key={i} className="p-4 rounded-2xl bg-white/5 border border-white/5 text-center">
|
||||
<div className="text-[8px] font-black text-gray-600 mb-1">{t.label}</div>
|
||||
<div className={`text-[10px] font-bold ${t.color}`}>{t.val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT COL: CHANGELOG */}
|
||||
<div className="space-y-6">
|
||||
<div className="glass-panel p-8 rounded-[2rem] border border-white/10 relative overflow-hidden h-full">
|
||||
<div className="absolute top-0 left-0 w-2 h-full bg-purple-500"></div>
|
||||
<h2 className="text-lg font-black text-purple-400 mb-8 uppercase tracking-widest">Evolution Logs</h2>
|
||||
|
||||
<div className="space-y-10 relative before:absolute before:left-[7px] before:top-2 before:bottom-2 before:w-[2px] before:bg-white/5">
|
||||
{[
|
||||
{
|
||||
v: 'V.4.2',
|
||||
t: 'QUANTUM BILLING',
|
||||
d: 'Mei 2026',
|
||||
c: 'bg-purple-500',
|
||||
items: ['Xendit Live Integration', '101 Module Matrix', 'Auto-Pilot v2.0']
|
||||
},
|
||||
{
|
||||
v: 'V.4.0',
|
||||
t: 'OMNIVERSAL CORE',
|
||||
d: 'April 2026',
|
||||
c: 'bg-emerald-500',
|
||||
items: ['eBPF Kernel Bypass', 'MoQ Transport', 'Deep-Twin AI']
|
||||
},
|
||||
{
|
||||
v: 'V.3.0',
|
||||
t: 'BONE SEPARATION',
|
||||
d: 'Maret 2026',
|
||||
c: 'bg-gray-700',
|
||||
items: ['Microservices Split', 'Multi-Tenant IAM', 'Rust SFU Alpha']
|
||||
}
|
||||
].map((log, i) => (
|
||||
<div key={i} className="relative pl-8">
|
||||
<div className={`absolute left-0 top-1.5 w-4 h-4 rounded-full ${log.c} shadow-[0_0_15px_rgba(168,85,247,0.3)] z-10 border-4 border-[#050b14]`}></div>
|
||||
<div className="text-[10px] font-black text-gray-500 mb-1">{log.d} — {log.v}</div>
|
||||
<h3 className="text-sm font-black text-white mb-3 tracking-tight">{log.t}</h3>
|
||||
<ul className="space-y-2">
|
||||
{log.items.map((item, idx) => (
|
||||
<li key={idx} className="text-[10px] text-gray-500 flex items-center gap-2">
|
||||
<span className="w-1 h-1 bg-gray-700 rounded-full"></span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface Pkg {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
features: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
licenseNumber?: string;
|
||||
packageId?: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
import { useOmni } from "@/components/OmniSyncProvider";
|
||||
|
||||
export default function BillingManagementPage() {
|
||||
const { t } = useDictionary();
|
||||
const { currency, setCurrency, formatCurrency, locale, setLocale, theme, setTheme } = useOmni();
|
||||
const [packages, setPackages] = useState<Pkg[]>([]);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState({ totalRevenue: 150000000, activeTenants: 0, growth: "+12.5%" });
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [pkgResp, eyeResp] = await Promise.all([
|
||||
fetch("/api/superadmin/packages"),
|
||||
fetch("/api/superadmin/supreme-dashboard")
|
||||
]);
|
||||
const pkgData = await pkgResp.json();
|
||||
const eyeData = await eyeResp.json();
|
||||
|
||||
setPackages(pkgData.packages || []);
|
||||
setTenants(eyeData.matrix || []);
|
||||
|
||||
// Calculate stats
|
||||
const activeCount = (eyeData.matrix || []).filter((t: Tenant) => t.isActive).length;
|
||||
setStats(prev => ({ ...prev, activeTenants: activeCount }));
|
||||
|
||||
} catch (_e) {
|
||||
console.error("Failed to load billing data");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleKillSwitch = async (id: string, name: string) => {
|
||||
if (!confirm(`ACTIVATE KILL-SWITCH: Deactivate [${name}] immediately?`)) return;
|
||||
try {
|
||||
const res = await fetch('/api/superadmin/tenants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'update', id, isActive: false })
|
||||
});
|
||||
if (res.ok) {
|
||||
setTenants(prev => prev.map(t => t.id === id ? { ...t, isActive: false } : t));
|
||||
}
|
||||
} catch (_e) {}
|
||||
};
|
||||
|
||||
const handleReactivate = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch('/api/superadmin/tenants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'update', id, isActive: true })
|
||||
});
|
||||
if (res.ok) {
|
||||
setTenants(prev => prev.map(t => t.id === id ? { ...t, isActive: true } : t));
|
||||
}
|
||||
} catch (_e) {}
|
||||
};
|
||||
|
||||
if (loading) return <div className="min-h-screen bg-[#050b14] flex items-center justify-center text-white font-mono animate-pulse">SYNCHRONIZING BILLING MATRIX...</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050b14] p-6 md:p-12 text-white overflow-x-hidden relative">
|
||||
{/* BACKGROUND DECORATION */}
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-500/10 rounded-full blur-[150px] -z-10"></div>
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-purple-500/10 rounded-full blur-[150px] -z-10"></div>
|
||||
|
||||
<div className="max-w-7xl mx-auto space-y-10">
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-amber-500 rounded-lg flex items-center justify-center shadow-[0_0_20px_rgba(245,158,11,0.4)]">
|
||||
<svg className="w-6 h-6 text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<h1 className="text-4xl font-black tracking-tighter bg-linear-to-r from-amber-400 to-amber-600 bg-clip-text text-transparent">XCU {t('Billing.title')}</h1>
|
||||
</div>
|
||||
<p className="text-gray-400 font-medium">Quantum Revenue & Subscription Orchestration Node.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* SETTINGS MATRIX */}
|
||||
<div className="flex items-center gap-2 p-1.5 glass-panel rounded-xl border-white/5 bg-black/20">
|
||||
<button onClick={() => setLocale(locale === 'id' ? 'en' : 'id')} className="px-3 py-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-[10px] font-black uppercase tracking-widest border border-white/5 transition-all">
|
||||
{locale === 'id' ? 'ID 🇮🇩' : 'EN 🇺🇸'}
|
||||
</button>
|
||||
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 border border-white/5 transition-all text-amber-500">
|
||||
{theme === 'dark' ? (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"></path></svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
)}
|
||||
</button>
|
||||
<select value={currency} onChange={(e) => setCurrency(e.target.value as any)} className="bg-transparent text-[10px] font-black uppercase tracking-widest border-none focus:ring-0 cursor-pointer text-emerald-500">
|
||||
<option value="Rp" className="bg-[#050b14]">IDR</option>
|
||||
<option value="USD" className="bg-[#050b14]">USD</option>
|
||||
<option value="Crypto" className="bg-[#050b14]">XCU</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Link href="/supreme-admin" className="px-6 py-3 glass-panel border border-white/10 hover:border-white/20 rounded-2xl font-bold transition-all hover:bg-white/5 flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
{t('Common.home')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* STATS GRID */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ label: "ESTIMATED REVENUE", value: formatCurrency(stats.totalRevenue), color: "text-emerald-400", sub: "Calculated Real-time" },
|
||||
{ label: "ACTIVE TENANTS", value: stats.activeTenants.toString(), color: "text-blue-400", sub: stats.growth + " this month" },
|
||||
{ label: "SYSTEM UPTIME", value: "99.999%", color: "text-purple-400", sub: "0ms Latency Logic" }
|
||||
].map((s, i) => (
|
||||
<div key={i} className="glass-panel p-6 rounded-3xl border border-white/5 bg-white/2 overflow-hidden relative group">
|
||||
<div className="absolute inset-0 bg-linear-to-br from-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<div className="text-[10px] font-black text-gray-500 tracking-[0.2em] mb-2">{s.label}</div>
|
||||
<div className={`text-3xl font-black ${s.color} tracking-tight`}>{s.value}</div>
|
||||
<div className="text-[10px] text-gray-500 mt-2">{s.sub}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ACTIVE PACKAGES */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-black tracking-widest text-gray-400 border-l-4 border-amber-500 pl-4">LIVE SUBSCRIPTION PLANS</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{packages.map(pkg => {
|
||||
const numericPrice = parseInt(pkg.price.replace(/[^0-9]/g, '') || '0');
|
||||
return (
|
||||
<div key={pkg.id} className="glass-panel p-8 rounded-[2rem] border border-white/10 relative overflow-hidden group hover:border-amber-500/50 transition-all">
|
||||
<div className="absolute -top-10 -right-10 w-32 h-32 bg-amber-500/5 rounded-full blur-3xl group-hover:bg-amber-500/10 transition-colors"></div>
|
||||
<h3 className="text-xl font-extrabold text-white mb-1">{pkg.name}</h3>
|
||||
<div className="text-2xl font-black text-amber-500 mb-6">{numericPrice === 0 ? 'PARTNER FREE' : formatCurrency(numericPrice)}</div>
|
||||
|
||||
<div className="space-y-3 mb-8">
|
||||
{JSON.parse(pkg.features || '[]').slice(0, 3).map((f: string, idx: number) => (
|
||||
<div key={idx} className="flex items-center gap-2 text-xs text-gray-400">
|
||||
<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.replace(/\./g, ' ').split(' ').slice(-1)[0].toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
{JSON.parse(pkg.features || '[]').length > 3 && (
|
||||
<div className="text-[10px] text-gray-600 font-bold">+{JSON.parse(pkg.features || '[]').length - 3} MORE CAPABILITIES</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-white/5">
|
||||
<div className="text-[10px] font-bold text-gray-500 uppercase tracking-widest">Status: <span className="text-emerald-500">PROVISIONED</span></div>
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TENANT TABLE */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-black tracking-widest text-gray-400 border-l-4 border-blue-500 pl-4">TENANT SUBSCRIPTION REGISTRY</h2>
|
||||
<div className="glass-panel rounded-[2rem] border border-white/5 overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-white/5 text-[10px] font-black text-gray-400 uppercase tracking-[0.2em]">
|
||||
<th className="px-8 py-6">Identity</th>
|
||||
<th className="px-8 py-6">License Number</th>
|
||||
<th className="px-8 py-6">Provision Date</th>
|
||||
<th className="px-8 py-6">Status</th>
|
||||
<th className="px-8 py-6 text-right">Emergency Control</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{tenants.map(t => (
|
||||
<tr key={t.id} className="hover:bg-white/[0.02] transition-colors group">
|
||||
<td className="px-8 py-6">
|
||||
<div className="text-sm font-bold text-white">{t.name}</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono mt-1">UID: {t.id.substring(0, 8)}...</div>
|
||||
</td>
|
||||
<td className="px-8 py-6">
|
||||
<span className="text-[10px] font-mono bg-blue-500/10 text-blue-400 px-3 py-1 rounded-full border border-blue-500/20">
|
||||
{t.licenseNumber || "N/A"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-8 py-6 text-xs text-gray-400 font-mono">
|
||||
{new Date(t.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-8 py-6">
|
||||
{t.isActive ? (
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-emerald-400 uppercase tracking-widest">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.5)]"></div>
|
||||
ACTIVE
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-red-500 uppercase tracking-widest">
|
||||
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
SUSPENDED
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-8 py-6 text-right">
|
||||
{t.isActive ? (
|
||||
<button
|
||||
onClick={() => handleKillSwitch(t.id, t.name)}
|
||||
className="text-[10px] font-black text-red-500 border border-red-500/30 px-4 py-2 rounded-xl hover:bg-red-500 hover:text-white transition-all shadow-[0_0_10px_rgba(239,68,68,0)] hover:shadow-[0_0_15px_rgba(239,68,68,0.5)] uppercase tracking-widest"
|
||||
>
|
||||
Kill Switch
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleReactivate(t.id)}
|
||||
className="text-[10px] font-black text-emerald-500 border border-emerald-500/30 px-4 py-2 rounded-xl hover:bg-emerald-500 hover:text-white transition-all uppercase tracking-widest"
|
||||
>
|
||||
Restore Access
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* FOOTER WATERMARK */}
|
||||
<div className="mt-20 text-center pb-10">
|
||||
<div className="text-[10px] font-black text-gray-700 tracking-[0.5em] uppercase">JUMPA.ID ULTRA BILLING SYSTEM V2.4</div>
|
||||
<div className="text-[8px] text-gray-800 mt-2 font-mono">ENCRYPTED QUANTUM HANDSHAKE: SUCCESS</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function MindmapPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => setMounted(true));
|
||||
}, []);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a101d] text-white p-8 relative overflow-hidden font-sans">
|
||||
{/* Background Matrix Effect */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-20">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(10,16,29,0)_50%,rgba(0,0,0,0.5)_50%),linear-gradient(90deg,rgba(0,255,136,0.03),rgba(168,85,247,0.03),rgba(59,130,246,0.03))] bg-size-[100%_4px,4px_100%] z-0 mix-blend-screen"></div>
|
||||
</div>
|
||||
|
||||
<header className="relative z-10 flex justify-between items-center mb-12 border-b border-white/10 pb-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-black tracking-tight bg-clip-text text-transparent bg-linear-to-r from-blue-400 via-purple-500 to-green-400">
|
||||
JUMPA.ID ULTRA MINDMAP
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-2 tracking-wide text-sm">Interactive Architecture Diagram (SaaS Ecosystem)</p>
|
||||
</div>
|
||||
<Link href="/supreme-admin" className="px-6 py-2 bg-white/5 hover:bg-white/10 border border-white/20 rounded-full font-bold transition-all flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
Kembali ke Dashboard
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
{/* SVG Mindmap Canvas */}
|
||||
<div className="relative z-10 w-full h-[75vh] bg-[#111827] rounded-3xl border border-gray-800 shadow-[0_0_50px_rgba(0,0,0,0.5)] overflow-hidden flex items-center justify-center p-10">
|
||||
|
||||
{/* Connection Lines */}
|
||||
<svg className="absolute inset-0 w-full h-full pointer-events-none" style={{ zIndex: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="line-iam-vc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#a855f7" />
|
||||
<stop offset="100%" stopColor="#3b82f6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="line-iam-chat" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor="#a855f7" />
|
||||
<stop offset="100%" stopColor="#10b981" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Line: IAM -> VC */}
|
||||
<path d="M 500 200 Q 750 200 750 400" fill="none" stroke="url(#line-iam-vc)" strokeWidth="3" strokeDasharray="10,10" className="animate-[dash_20s_linear_infinite]" />
|
||||
|
||||
{/* Line: IAM -> Chat */}
|
||||
<path d="M 500 200 Q 250 200 250 400" fill="none" stroke="url(#line-iam-chat)" strokeWidth="3" strokeDasharray="10,10" className="animate-[dash_20s_linear_infinite_reverse]" />
|
||||
|
||||
{/* Line: Chat <-> VC */}
|
||||
<path d="M 250 400 Q 500 600 750 400" fill="none" stroke="#4b5563" strokeWidth="2" strokeDasharray="5,5" className="animate-[dash_10s_linear_infinite]" />
|
||||
</svg>
|
||||
|
||||
<div className="relative w-full h-full max-w-5xl" style={{ zIndex: 1 }}>
|
||||
|
||||
{/* Node: IAM */}
|
||||
<div className="absolute top-[10%] left-1/2 -translate-x-1/2 group">
|
||||
<div className="w-64 bg-purple-900/30 backdrop-blur-md border-2 border-purple-500 rounded-2xl p-6 shadow-[0_0_30px_rgba(168,85,247,0.3)] group-hover:scale-105 transition-transform cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-purple-500 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-purple-400">JUMPA IAM</h3>
|
||||
</div>
|
||||
<p className="text-xs text-purple-200">Identity & Access Management. Pusat Otorisasi Zero-Trust & Billing.</p>
|
||||
<ul className="mt-4 text-[10px] space-y-1 text-purple-300 font-mono">
|
||||
<li>• QR Code Login</li>
|
||||
<li>• Billing Management</li>
|
||||
<li>• White-Label Versioning</li>
|
||||
<li>• Token Issuer (PostgreSQL)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node: Chat */}
|
||||
<div className="absolute top-[50%] left-[10%] group">
|
||||
<div className="w-64 bg-emerald-900/30 backdrop-blur-md border-2 border-emerald-500 rounded-2xl p-6 shadow-[0_0_30px_rgba(16,185,129,0.3)] group-hover:scale-105 transition-transform cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-emerald-500 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-emerald-400">JUMPA CHAT</h3>
|
||||
</div>
|
||||
<p className="text-xs text-emerald-200">WebSocket Server (Node.js/Socket.io). Sistem Komunikasi Persistent.</p>
|
||||
<ul className="mt-4 text-[10px] space-y-1 text-emerald-300 font-mono">
|
||||
<li>• Omni-Brain AI Interceptor</li>
|
||||
<li>• The Vault (DRM Storage)</li>
|
||||
<li>• End-to-End Encryption</li>
|
||||
<li>• FFmpeg Multi-Stream Engine</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Node: VC */}
|
||||
<div className="absolute top-[50%] right-[10%] group">
|
||||
<div className="w-64 bg-blue-900/30 backdrop-blur-md border-2 border-blue-500 rounded-2xl p-6 shadow-[0_0_30px_rgba(59,130,246,0.3)] group-hover:scale-105 transition-transform cursor-pointer">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center shadow-lg">
|
||||
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-blue-400">JUMPA VC</h3>
|
||||
</div>
|
||||
<p className="text-xs text-blue-200">XCU Ultra eBPF Engine. Konferensi Video Ultra-Low Latency AV1.</p>
|
||||
<ul className="mt-4 text-[10px] space-y-1 text-blue-300 font-mono">
|
||||
<li>• AI Deep-Twin Surrogate</li>
|
||||
<li>• Breakout Matrix</li>
|
||||
<li>• Supreme Eye (Multiverse CCTV)</li>
|
||||
<li>• TTE Live Canvas</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
`}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,875 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface Feature {
|
||||
key: string;
|
||||
name: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
interface Package {
|
||||
id: string;
|
||||
name: string;
|
||||
price: string;
|
||||
features: string;
|
||||
}
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
licenseNumber: string;
|
||||
licenses?: string | Record<string, string>;
|
||||
package?: {
|
||||
id: string;
|
||||
name: string;
|
||||
features: string;
|
||||
};
|
||||
byokEnabled?: boolean;
|
||||
byokKey?: string;
|
||||
users?: any[];
|
||||
}
|
||||
|
||||
interface User {
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export default function SupremeAdminDashboard() {
|
||||
const [data, setData] = useState<User | null>(null);
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [packages, setPackages] = useState<Package[]>([]);
|
||||
const [systemFeatures, setSystemFeatures] = useState<Feature[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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 = '/';
|
||||
}
|
||||
};
|
||||
|
||||
// Modal States
|
||||
const [showPackageModal, setShowPackageModal] = useState(false);
|
||||
const [showTenantModal, setShowTenantModal] = useState(false);
|
||||
const [selectedTenant, setSelectedTenant] = useState<Tenant | null>(null);
|
||||
|
||||
// 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 [tenantLicenses, setTenantLicenses] = useState<Record<string, string>>({});
|
||||
const [byokEnabled, setByokEnabled] = useState(false);
|
||||
const [byokKey, setByokKey] = useState("");
|
||||
|
||||
// Form States
|
||||
const [editingPkgId, setEditingPkgId] = useState<string | null>(null);
|
||||
const [newPkgName, setNewPkgName] = useState("");
|
||||
const [newPkgPrice, setNewPkgPrice] = useState("");
|
||||
const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// XCU Engine State
|
||||
const [xcuNodes, setXcuNodes] = useState<any[]>([]);
|
||||
const [xcuLoading, setXcuLoading] = useState(false);
|
||||
|
||||
// Security Tier Modal
|
||||
const [tierModal, setTierModal] = useState<{tenantId: string; tenantName: string; currentTier: string} | null>(null);
|
||||
const [tierSwitching, setTierSwitching] = useState(false);
|
||||
const [selectedTier, setSelectedTier] = useState('STANDARD');
|
||||
|
||||
const TIER_CONFIG: Record<string, {label:string; desc:string; color:string; bg:string; border:string; icon:string}> = {
|
||||
STANDARD: { label: "Let's Encrypt", desc: 'Sertifikat global, dipercaya semua browser. Auto-renew 90 hari.', color: 'text-emerald-400', bg: 'bg-emerald-500/10', border: 'border-emerald-500/20', icon: '🌐' },
|
||||
SOVEREIGN: { label: 'Private CA (X)', desc: 'RSA-4096, 30 tahun. Offline-ready, tidak bergantung internet global.', color: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20', icon: '🛡️' },
|
||||
CLIENT_CA: { label: 'Client CA (Upload)', desc: 'Tenant upload CA mereka sendiri (BSSN/Komdigi/Militer/BIN).', color: 'text-amber-400', bg: 'bg-amber-500/10', border: 'border-amber-500/20', icon: '🏛️' },
|
||||
};
|
||||
|
||||
const fetchXcuStatus = useCallback(async () => {
|
||||
setXcuLoading(true);
|
||||
try {
|
||||
const resp = await fetch('/api/superadmin/xcu-tls-switch');
|
||||
const data = await resp.json();
|
||||
setXcuNodes(data.nodes || []);
|
||||
} catch (_e) {
|
||||
console.error('XCU fetch failed', _e);
|
||||
} finally {
|
||||
setXcuLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
const [pkgResp, eyeResp] = await Promise.all([
|
||||
fetch("/api/superadmin/packages"),
|
||||
fetch("/api/superadmin/supreme-dashboard")
|
||||
]);
|
||||
const pkgData = await pkgResp.json();
|
||||
const eyeData = await eyeResp.json();
|
||||
|
||||
if (eyeData.error) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
setPackages(pkgData.packages || []);
|
||||
setTenants(eyeData.matrix || []);
|
||||
setSystemFeatures(pkgData.systemFeatures || []);
|
||||
setData(eyeData.user || { email: "Supreme Admin", role: "superadmin" });
|
||||
} catch (_e) {
|
||||
console.error(_e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
// Live polling XCU Engine status every 5s
|
||||
const pollXcu = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/superadmin/xcu-tls-switch');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
if (data.nodes) setXcuNodes(data.nodes);
|
||||
}
|
||||
} catch {}
|
||||
setXcuLoading(false);
|
||||
};
|
||||
pollXcu();
|
||||
const xcuInterval = setInterval(pollXcu, 5000);
|
||||
return () => clearInterval(xcuInterval);
|
||||
}, [fetchData]);
|
||||
|
||||
const handleSavePackage = async (e?: React.FormEvent | React.MouseEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
try {
|
||||
const resp = await fetch("/api/superadmin/packages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: editingPkgId ? "update" : "create",
|
||||
id: editingPkgId,
|
||||
name: newPkgName,
|
||||
price: newPkgPrice,
|
||||
features: selectedFeatures
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
setShowPackageModal(false);
|
||||
setEditingPkgId(null);
|
||||
fetchData();
|
||||
showToast(editingPkgId ? "Paket berhasil diperbarui." : "Paket berhasil dibuat.");
|
||||
} else {
|
||||
const errorData = await resp.json();
|
||||
showToast(errorData.error || "Gagal menyimpan paket.", "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Error menyimpan paket. Periksa koneksi Anda.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePackage = async (id: string) => {
|
||||
try {
|
||||
const resp = await fetch("/api/superadmin/packages", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "delete", id })
|
||||
});
|
||||
if (resp.ok) {
|
||||
fetchData();
|
||||
showToast("Paket berhasil dihapus.");
|
||||
} else {
|
||||
const errorData = await resp.json();
|
||||
showToast(errorData.error || "Gagal menghapus paket.", "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Error menghapus paket. Periksa koneksi Anda.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const saveTenantPackage = async (tenantId: string, packageId: string) => {
|
||||
try {
|
||||
const resp = await fetch("/api/superadmin/supreme-dashboard", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "update_tenant_package",
|
||||
tenantId,
|
||||
packageId
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
fetchData();
|
||||
showToast("Paket tenant berhasil diperbarui.");
|
||||
} else {
|
||||
const errorData = await resp.json();
|
||||
showToast(errorData.error || "Gagal menetapkan paket.", "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Gagal menetapkan paket. Periksa koneksi.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const openTenantMatrix = (tenant: Tenant) => {
|
||||
setSelectedTenant(tenant);
|
||||
setByokEnabled(tenant.byokEnabled || false);
|
||||
setByokKey(tenant.byokKey || "");
|
||||
let parsed: Record<string, string> = {};
|
||||
try {
|
||||
if (typeof tenant.licenses === 'string') parsed = JSON.parse(tenant.licenses);
|
||||
else parsed = tenant.licenses || {};
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
const temp: Record<string, string> = {};
|
||||
parsed.forEach((k: string) => temp[k] = "GRANTED");
|
||||
parsed = temp;
|
||||
}
|
||||
} catch(_e) {}
|
||||
setTenantLicenses(parsed);
|
||||
setShowTenantModal(true);
|
||||
};
|
||||
|
||||
const [isSavingMatrix, setIsSavingMatrix] = useState(false);
|
||||
|
||||
const saveTenantMatrix = async () => {
|
||||
setIsSavingMatrix(true);
|
||||
try {
|
||||
const resp = await fetch("/api/superadmin/supreme-dashboard", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "update_tenant_licenses",
|
||||
tenantId: selectedTenant?.id,
|
||||
licenses: tenantLicenses,
|
||||
byokEnabled,
|
||||
byokKey
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
setShowTenantModal(false);
|
||||
fetchData();
|
||||
showToast("Matriks Sovereignty berhasil di-deploy tanpa henti (Zero Downtime).");
|
||||
} else {
|
||||
const errorData = await resp.json();
|
||||
showToast(errorData.error || "Gagal menyimpan matrix.", "error");
|
||||
}
|
||||
} catch (_e) {
|
||||
showToast("Koneksi terputus. Gagal menyimpan matrix.", "error");
|
||||
} finally {
|
||||
setIsSavingMatrix(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applySecurityTier = async () => {
|
||||
if (!tierModal || selectedTier === tierModal.currentTier) return;
|
||||
setTierSwitching(true);
|
||||
try {
|
||||
const resp = await fetch('/api/superadmin/supreme-dashboard', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'update_security_tier', tenantId: tierModal.tenantId, securityTier: selectedTier }),
|
||||
});
|
||||
if (resp.ok) {
|
||||
fetchData();
|
||||
showToast(`Security Tier berhasil di-switch ke ${TIER_CONFIG[selectedTier]?.label || selectedTier}`);
|
||||
setTierModal(null);
|
||||
} else {
|
||||
showToast('Gagal mengubah Security Tier', 'error');
|
||||
}
|
||||
} catch { showToast('Koneksi gagal', 'error'); }
|
||||
setTierSwitching(false);
|
||||
};
|
||||
|
||||
if (loading) return <div className="min-h-screen bg-[#f4f5f9] flex items-center justify-center text-blue-600 font-medium">Loading Supreme Admin...</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 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">Supreme Admin <span className="text-sm font-normal text-gray-500 ml-2">Workspace Control</span></h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<a href="/supreme-admin/telepathy" className="px-3 py-1.5 bg-gradient-to-r from-[#ff0080] to-[#7928ca] 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>
|
||||
TELEPATHY MATRIX
|
||||
</a>
|
||||
<a href="/supreme-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>
|
||||
PANOPTICON
|
||||
</a>
|
||||
<span className="text-sm text-gray-600 font-medium">{data?.email}</span>
|
||||
<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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Total Tenants</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{tenants.length}</div>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">XCU Modules</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{systemFeatures.length || '101'}</div>
|
||||
</div>
|
||||
<div className="bg-white p-5 rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-1">Active Packages</div>
|
||||
<div className="text-3xl font-bold text-gray-900">{packages.length}</div>
|
||||
</div>
|
||||
<div className="bg-blue-600 p-5 rounded-lg border border-blue-700 shadow-sm text-white flex flex-col justify-center cursor-pointer hover:bg-blue-700 transition-colors" onClick={() => { setEditingPkgId(null); setNewPkgName(""); setNewPkgPrice(""); setSelectedFeatures([]); setShowPackageModal(true); }}>
|
||||
<div className="text-sm font-bold flex items-center justify-between">
|
||||
Create New Package
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XCU QUIC ENGINE STATUS PANEL */}
|
||||
<div className="mb-8 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 rounded-2xl border border-slate-700/50 shadow-xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-700/50 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center shadow-lg shadow-cyan-500/20">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-white tracking-tight">XCom ULTRA Engine</h2>
|
||||
<p className="text-[10px] text-slate-400 font-mono">QUIC/WebTransport • Port 8443 (UDP) • Port 8081 (HTTP)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-red-500/10 border border-red-500/20 rounded-lg">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-500 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-red-500"></span>
|
||||
</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-red-400">LIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{xcuNodes.length > 0 ? xcuNodes.map((node: any, idx: number) => (
|
||||
<div key={idx} className={`relative rounded-xl border p-4 transition-all duration-300 ${
|
||||
node.online
|
||||
? 'bg-slate-800/80 border-slate-600/50 hover:border-cyan-500/50 hover:shadow-lg hover:shadow-cyan-500/5'
|
||||
: 'bg-red-950/30 border-red-800/30'
|
||||
}`}>
|
||||
{/* Pulse indicator */}
|
||||
<div className="absolute top-3 right-3">
|
||||
<span className={`relative flex h-3 w-3`}>
|
||||
{node.online && <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-3 w-3 ${node.online ? 'bg-emerald-400' : 'bg-red-500'}`}></span>
|
||||
</span>
|
||||
</div>
|
||||
{/* Node name */}
|
||||
<div className="text-xs font-black text-slate-400 uppercase tracking-widest mb-2">{node.name}</div>
|
||||
<div className="text-[10px] font-mono text-slate-500 mb-3">{node.host}</div>
|
||||
{node.online ? (
|
||||
<>
|
||||
{/* TLS Mode Badge */}
|
||||
<div className="mb-3">
|
||||
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold uppercase tracking-wider ${
|
||||
node.tlsMode === 'SELFSIGNED'
|
||||
? '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 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
TLS: {node.tlsMode || 'LETSENCRYPT'}
|
||||
</div>
|
||||
</div>
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||
<div className="bg-slate-900/50 rounded-lg p-2">
|
||||
<div className="text-[9px] text-slate-500 uppercase font-bold">CPU</div>
|
||||
<div className="text-sm font-bold text-cyan-400">{typeof node.cpu === 'number' ? node.cpu.toFixed(1) : '0'}%</div>
|
||||
</div>
|
||||
<div className="bg-slate-900/50 rounded-lg p-2">
|
||||
<div className="text-[9px] text-slate-500 uppercase font-bold">RAM</div>
|
||||
<div className="text-sm font-bold text-purple-400">{typeof node.ram === 'number' ? node.ram.toFixed(1) : '0'}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Cert hash */}
|
||||
<div className="text-[8px] font-mono text-slate-600 truncate" title={node.certHash}>CERT: {node.certHash?.substring(0, 16)}...</div>
|
||||
{/* Status */}
|
||||
<div className={`mt-2 text-[10px] font-bold uppercase tracking-wider ${
|
||||
node.status === 'SECURE' ? 'text-emerald-400' : node.status === 'OUROBOROS_IGNITED' ? 'text-red-400' : 'text-slate-400'
|
||||
}`}>
|
||||
● {node.status}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-sm font-bold text-red-400 mt-2">● OFFLINE</div>
|
||||
)}
|
||||
</div>
|
||||
)) : (
|
||||
<div className="col-span-3 text-center py-8">
|
||||
<div className="text-slate-500 text-sm">Loading XCU Engine status...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<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">Client Organizations (Tenants)</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{tenants.map((t: Tenant) => {
|
||||
const isVIP = t.name.toLowerCase().includes('tsm') || t.name.toLowerCase().includes('snowy');
|
||||
return (
|
||||
<div key={t.id} className="flex flex-col border-b border-gray-100 last:border-0">
|
||||
<div className="p-6 hover:bg-gray-50 transition-colors flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h3 className="text-base font-bold text-gray-900">{t.name}</h3>
|
||||
{isVIP && <span className="px-2 py-0.5 bg-blue-100 text-blue-700 text-[10px] font-bold rounded uppercase tracking-wider">VIP Partner</span>}
|
||||
{/* Security Tier Badge */}
|
||||
{(() => {
|
||||
const tier = (t as any).securityTier || 'STANDARD';
|
||||
const badgeMap: Record<string,string> = {
|
||||
STANDARD: 'bg-emerald-50 text-emerald-600 border-emerald-200',
|
||||
SOVEREIGN: 'bg-cyan-50 text-cyan-700 border-cyan-200',
|
||||
CLIENT_CA: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||
};
|
||||
const labelMap: Record<string,string> = { STANDARD: '🌐 LET\'S ENCRYPT', SOVEREIGN: '🛡️ PRIVATE CA', CLIENT_CA: '🏛️ CLIENT CA' };
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { setSelectedTier(tier); setTierModal({ tenantId: t.id, tenantName: t.name, currentTier: tier }); }}
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider transition-all hover:scale-105 border ${badgeMap[tier] || badgeMap.STANDARD}`}
|
||||
title="Klik untuk ubah Security Tier"
|
||||
>
|
||||
{labelMap[tier] || '🌐 STANDARD'}
|
||||
</button>
|
||||
{(tier === 'SOVEREIGN' || tier === 'CLIENT_CA') && (
|
||||
<a href="/supreme-admin/sovereign-setup" className={`px-2 py-0.5 text-white text-[10px] font-bold rounded transition-colors ${tier === 'CLIENT_CA' ? 'bg-amber-500 hover:bg-amber-600' : 'bg-cyan-600 hover:bg-cyan-700'}`}>CA Setup →</a>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 font-mono mb-2 flex items-center gap-2">
|
||||
<svg className="w-3.5 h-3.5 text-gray-400" 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"></path></svg>
|
||||
License: <strong className="text-gray-700">{t.licenseNumber || 'PENDING'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col sm:items-end gap-3 w-full sm:w-auto">
|
||||
<div className="flex flex-col items-end gap-1 w-full">
|
||||
<span className="text-[10px] font-bold text-gray-500 uppercase">Assigned Package</span>
|
||||
<select
|
||||
className="text-xs border rounded px-2 py-1 bg-gray-50 w-full sm:w-48 outline-none focus:ring-1 focus:ring-blue-500"
|
||||
value={t.package?.id || ""}
|
||||
onChange={e => saveTenantPackage(t.id, e.target.value)}
|
||||
>
|
||||
<option value="">-- No Package (Custom) --</option>
|
||||
{packages.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => openTenantMatrix(t)} className="px-4 py-2 bg-white border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors self-start sm:self-auto">
|
||||
Manage Feature Matrix & BYOK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Render Users List for the Tenant */}
|
||||
{t.users && t.users.length > 0 && (
|
||||
<div className="bg-gray-50 border-t border-gray-100 p-4 pl-10">
|
||||
<div className="text-[11px] font-bold text-gray-500 uppercase tracking-widest mb-2 flex items-center gap-2">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
||||
Registered Neural Entities
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{t.users.map((u: any) => (
|
||||
<div key={u.id} className="flex justify-between items-center bg-white border border-gray-200 p-2 rounded-md shadow-sm">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-semibold text-gray-800">{u.email}</span>
|
||||
<span className="text-[10px] text-gray-400 font-mono">{u.id}</span>
|
||||
</div>
|
||||
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase ${u.role === 'admin' ? 'bg-purple-100 text-purple-700' : u.role === 'superadmin' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}`}>
|
||||
{u.role}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-6 py-5 border-b border-gray-200 bg-gray-50">
|
||||
<h2 className="text-lg font-semibold text-gray-800">Module Packages</h2>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{packages.map((p: Package) => {
|
||||
let featList: string[] = [];
|
||||
try {
|
||||
featList = JSON.parse(p.features || "[]");
|
||||
} catch (e) {}
|
||||
|
||||
const isJVC = p.name.includes("JVC");
|
||||
const isJC = p.name.includes("JC");
|
||||
const isCombo = isJVC && isJC;
|
||||
|
||||
const bgClass = isCombo ? "bg-gradient-to-br from-blue-800 via-teal-700 to-emerald-600 text-white border-none shadow-2xl shadow-teal-900/40 ring-1 ring-teal-400/50" : isJVC ? "bg-gradient-to-br from-indigo-950 via-blue-900 to-indigo-900 text-white border-none shadow-xl shadow-blue-900/20" : isJC ? "bg-gradient-to-br from-teal-950 via-emerald-900 to-teal-900 text-white border-none shadow-xl shadow-emerald-900/20" : "bg-white border border-gray-200 text-gray-900 shadow-sm";
|
||||
const badgeClass = isCombo ? "bg-white/20 text-white border border-white/30 font-bold drop-shadow-sm" : isJVC ? "bg-blue-500/20 text-blue-100 border border-blue-400/30" : isJC ? "bg-emerald-500/20 text-emerald-100 border border-emerald-400/30" : "bg-gray-50 text-gray-700 border border-gray-200";
|
||||
const textMuted = isJVC || isJC || isCombo ? "text-gray-300" : "text-gray-500";
|
||||
|
||||
return (
|
||||
<div key={p.id} className={`p-5 rounded-2xl transition-all duration-300 hover:-translate-y-1 relative overflow-hidden group ${bgClass}`}>
|
||||
{/* Decorative background glow */}
|
||||
{(isJVC || isJC || isCombo) && <div className="absolute -top-12 -right-12 w-40 h-40 bg-white opacity-[0.03] blur-3xl rounded-full pointer-events-none group-hover:opacity-10 transition-opacity"></div>}
|
||||
{isCombo && <div className="absolute -bottom-10 -left-10 w-32 h-32 bg-emerald-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
||||
{isCombo && <div className="absolute -top-10 -right-10 w-32 h-32 bg-blue-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
||||
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<div className="font-bold text-lg tracking-tight flex items-center gap-2 drop-shadow-md">
|
||||
{isCombo && <svg className="w-5 h-5 text-teal-100" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>}
|
||||
{p.name}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1.5">
|
||||
<div className={`text-sm font-black ${isJVC || isJC || isCombo ? 'text-white' : 'text-blue-600'}`}>{p.price}</div>
|
||||
<div className="flex gap-3 mt-1">
|
||||
<button onClick={() => { setEditingPkgId(p.id); setNewPkgName(p.name); setNewPkgPrice(p.price); setSelectedFeatures(featList); setShowPackageModal(true); }} className={`text-[10px] font-bold uppercase tracking-wider hover:underline ${textMuted}`}>Edit</button>
|
||||
<button onClick={() => handleDeletePackage(p.id)} className={`text-[10px] font-bold uppercase tracking-wider hover:underline ${isJVC || isJC || isCombo ? 'text-red-300' : 'text-red-500'}`}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-[10px] font-bold uppercase tracking-widest mb-3 border-t pt-4 ${isJVC || isJC || isCombo ? 'border-white/10 text-white/50' : 'border-gray-100 text-gray-400'}`}>
|
||||
Core Capabilities ({featList.length} Modules)
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 relative z-10">
|
||||
{featList.length > 0 ? (
|
||||
featList.map((f: string) => (
|
||||
<span key={f} className={`text-[9.5px] font-mono px-2 py-1 rounded-md backdrop-blur-md ${badgeClass}`}>
|
||||
{f}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className={`text-[10px] italic ${textMuted}`}>No modules defined</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* CREATE PACKAGE MODAL */}
|
||||
{showPackageModal && (
|
||||
<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 flex justify-between items-center bg-gray-50">
|
||||
<h2 className="text-xl font-bold text-gray-900">Create XCU Module Package</h2>
|
||||
<button onClick={() => setShowPackageModal(false)} className="text-gray-400 hover:text-gray-700">✕</button>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1">
|
||||
<div id="pkgForm" className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input type="text" value={newPkgName} onChange={e => setNewPkgName(e.target.value)} placeholder="Package Name" className="border p-2 rounded" required />
|
||||
<input type="text" value={newPkgPrice} onChange={e => setNewPkgPrice(e.target.value)} placeholder="Price (Rp)" className="border p-2 rounded" required />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4 border-b pb-2">
|
||||
<h3 className="text-sm font-bold">Select Modules</h3>
|
||||
<input type="text" placeholder="Search..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="text-xs border rounded-full px-3 py-1 outline-none" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
||||
const filtered = systemFeatures.filter(f => (f.module === mod || (!f.module && mod === 'IAM')) && (f.name.toLowerCase().includes(searchQuery.toLowerCase()) || f.key.toLowerCase().includes(searchQuery.toLowerCase())));
|
||||
if (filtered.length === 0) return null;
|
||||
return (
|
||||
<div key={mod}>
|
||||
<div className="text-[10px] font-black text-blue-600 mb-2 flex justify-between">
|
||||
<span>{mod} MODULES</span>
|
||||
<div className="space-x-2">
|
||||
<button type="button" onClick={() => setSelectedFeatures(Array.from(new Set([...selectedFeatures, ...systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).map(f => f.key)])))} className="hover:underline">Select All</button>
|
||||
<button type="button" onClick={() => setSelectedFeatures(selectedFeatures.filter(k => !systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).some(f => f.key === k)))} className="hover:underline">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{filtered.map(f => (
|
||||
<label key={f.key} className="flex items-center gap-2 p-2 border rounded hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" checked={selectedFeatures.includes(f.key)} onChange={e => e.target.checked ? setSelectedFeatures([...selectedFeatures, f.key]) : setSelectedFeatures(selectedFeatures.filter(x => x !== f.key))} />
|
||||
<div className="text-[10px]">{f.name}</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 border-t bg-gray-50 flex justify-end gap-2">
|
||||
<button type="button" onClick={() => setShowPackageModal(false)} className="px-4 py-2 border rounded">Cancel</button>
|
||||
<button type="button" onClick={handleSavePackage} className="px-4 py-2 bg-blue-600 text-white rounded">Save Package</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TENANT MATRIX MODAL */}
|
||||
{showTenantModal && selectedTenant && (
|
||||
<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-3xl rounded-xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<div className="px-6 py-4 border-b bg-gray-50 flex justify-between items-center relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-64 h-full bg-gradient-to-l from-blue-100/50 to-transparent pointer-events-none"></div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-gray-900 tracking-tight flex items-center gap-3">
|
||||
{selectedTenant.name}
|
||||
<span className="px-3 py-1 bg-gray-900 text-white text-[10px] font-bold rounded-full uppercase tracking-widest shadow-sm">
|
||||
{selectedTenant.package ? selectedTenant.package.name : "A LA CARTE (CUSTOM)"}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 font-medium mt-1">Superadmin Matrix Control & Sovereignty Panel</p>
|
||||
</div>
|
||||
<button onClick={() => setShowTenantModal(false)} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-gray-200 text-gray-500 transition-colors z-10">✕</button>
|
||||
</div>
|
||||
<div className="p-6 overflow-y-auto flex-1 bg-gradient-to-b from-gray-50/50 to-white">
|
||||
|
||||
{/* SPECTACULAR LICENSE TIER BANNER */}
|
||||
<div className={`mb-6 relative overflow-hidden rounded-2xl p-6 border ${selectedTenant.package ? 'bg-gradient-to-br from-indigo-950 via-blue-900 to-indigo-900 border-none shadow-xl shadow-blue-900/20 text-white' : 'bg-white border-dashed border-gray-300 text-gray-800'}`}>
|
||||
{selectedTenant.package && <div className="absolute -top-24 -right-24 w-64 h-64 bg-blue-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
||||
{selectedTenant.package && <div className="absolute -bottom-24 -left-24 w-64 h-64 bg-cyan-400 opacity-20 blur-3xl rounded-full pointer-events-none"></div>}
|
||||
|
||||
<div className="relative z-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<div className={`text-[10px] font-black uppercase tracking-widest mb-1 ${selectedTenant.package ? 'text-blue-300' : 'text-gray-400'}`}>Active Quantum Tier</div>
|
||||
<div className="text-2xl font-black flex items-center gap-2">
|
||||
{selectedTenant.package ? (
|
||||
<>
|
||||
<svg className="w-6 h-6 text-cyan-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l10-7.27z"/></svg>
|
||||
{selectedTenant.package.name}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
|
||||
Custom A La Carte
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className={`text-xs mt-2 max-w-xl ${selectedTenant.package ? 'text-blue-100' : 'text-gray-500'}`}>
|
||||
{selectedTenant.package ?
|
||||
"Modul-modul di bawah ini otomatis di-kunci (GRANTED) berdasarkan paket yang dipilih. Anda dapat menambahkan modul tambahan sebagai A La Carte." :
|
||||
"Tenant ini tidak terikat pada paket apapun. Semua modul diatur secara manual per-item melalui saklar di bawah ini."}
|
||||
</p>
|
||||
</div>
|
||||
{selectedTenant.package && (
|
||||
<div className="px-4 py-2 bg-white/10 backdrop-blur border border-white/20 rounded-xl text-xs font-bold text-blue-50">
|
||||
Paket Terkunci (Auto-Granted)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-blue-50/80 p-6 rounded-2xl border border-blue-100 shadow-inner">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-sm font-black text-blue-900 uppercase tracking-widest mb-1 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" 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>
|
||||
Quantum BYOK Sovereignty
|
||||
</h3>
|
||||
<p className="text-[10px] text-blue-700 font-medium">Berikan otoritas penuh kepada tenant untuk mendikte kunci enkripsi E2EE mereka sendiri.</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 w-full md:w-64">
|
||||
<label className="flex items-center gap-3 cursor-pointer bg-white px-4 py-3 rounded-xl border border-blue-200 shadow-sm hover:border-blue-400 transition-colors">
|
||||
<input type="checkbox" checked={byokEnabled} onChange={e => setByokEnabled(e.target.checked)} className="w-4 h-4 text-blue-600 rounded focus:ring-blue-500" />
|
||||
<span className="text-[10px] font-bold text-gray-700">Enable BYOK Mode</span>
|
||||
</label>
|
||||
{byokEnabled && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter Custom Quantum Key..."
|
||||
value={byokKey}
|
||||
onChange={e => setByokKey(e.target.value)}
|
||||
className="text-[10px] font-mono bg-white border border-blue-200 rounded-xl px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<input type="text" placeholder="Filter Matrix..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="border rounded-full px-4 py-2 text-sm w-64 outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{['JVC', 'JC', 'XCU', 'XTM', 'IAM'].map(mod => {
|
||||
const filtered = systemFeatures.filter(f => (f.module === mod || (!f.module && mod === 'IAM')) && (f.name.toLowerCase().includes(searchQuery.toLowerCase()) || f.key.toLowerCase().includes(searchQuery.toLowerCase())));
|
||||
if (filtered.length === 0) return null;
|
||||
return (
|
||||
<div key={mod}>
|
||||
<div className="flex justify-between items-center mb-4 border-l-4 border-blue-600 pl-3">
|
||||
<span className="text-[10px] font-black text-blue-600">{mod} MODULES</span>
|
||||
<div className="space-x-2">
|
||||
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'GRANTED'); setTenantLicenses(u); }} className="text-[8px] font-bold text-emerald-600 bg-emerald-50 px-2 py-1 rounded">Grant All</button>
|
||||
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'UPSELL'); setTenantLicenses(u); }} className="text-[8px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded">Upsell All</button>
|
||||
<button type="button" onClick={() => { const u = {...tenantLicenses}; systemFeatures.filter(f => f.module === mod || (!f.module && mod === 'IAM')).forEach(f => u[f.key] = 'HIDDEN'); setTenantLicenses(u); }} className="text-[8px] font-bold text-red-600 bg-red-50 px-2 py-1 rounded">Block All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{filtered.map(feat => {
|
||||
let isIncludedInPackage = false;
|
||||
if (selectedTenant?.package) {
|
||||
try {
|
||||
const pkgFeats = JSON.parse(selectedTenant.package.features || "[]");
|
||||
if (pkgFeats.includes(feat.key)) isIncludedInPackage = true;
|
||||
} catch(e) {}
|
||||
}
|
||||
const val = isIncludedInPackage ? 'GRANTED' : (tenantLicenses[feat.key] || 'HIDDEN');
|
||||
|
||||
return (
|
||||
<div key={feat.key} className={`flex justify-between items-center p-3 border rounded-lg ${isIncludedInPackage ? 'bg-gray-50 border-gray-200 opacity-70' : 'hover:border-blue-200'}`}>
|
||||
<div className="text-sm font-bold">{feat.name} <span className="text-[9px] font-mono text-gray-400 block">{feat.key}</span></div>
|
||||
<div className="flex items-center gap-3">
|
||||
{isIncludedInPackage && <span className="text-[9px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 uppercase">Included in Package</span>}
|
||||
<select
|
||||
value={val}
|
||||
onChange={e => setTenantLicenses({...tenantLicenses, [feat.key]: e.target.value})}
|
||||
className="text-xs font-bold p-1 rounded border"
|
||||
disabled={isIncludedInPackage}
|
||||
>
|
||||
<option value="GRANTED">🟢 GRANTED</option>
|
||||
<option value="UPSELL">💎 UPSELL</option>
|
||||
<option value="HIDDEN">❌ HIDDEN</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 border-t bg-gray-50 flex justify-end gap-3 relative z-50 shadow-[0_-10px_20px_-10px_rgba(0,0,0,0.05)]">
|
||||
<button type="button" onClick={(e) => { e.preventDefault(); e.stopPropagation(); setShowTenantModal(false); }} disabled={isSavingMatrix} className="px-6 py-2.5 border border-gray-300 rounded-xl hover:bg-gray-100 text-gray-700 font-bold transition-colors cursor-pointer relative z-50">Cancel</button>
|
||||
<button type="button" onClick={(e) => { e.preventDefault(); e.stopPropagation(); saveTenantMatrix(); }} disabled={isSavingMatrix} className={`px-6 py-2.5 rounded-xl font-bold transition-all shadow-lg cursor-pointer relative z-50 flex items-center gap-2 ${isSavingMatrix ? 'bg-blue-400 text-white cursor-not-allowed' : 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-blue-500/30 hover:-translate-y-0.5'}`}>
|
||||
{isSavingMatrix ? (
|
||||
<>
|
||||
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
|
||||
Menyimpan...
|
||||
</>
|
||||
) : 'Deploy Matrix'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SECURITY TIER SWITCH MODAL */}
|
||||
{tierModal && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-md" onClick={() => !tierSwitching && setTierModal(null)}>
|
||||
<div className="bg-gradient-to-b from-slate-900 to-slate-950 w-full max-w-lg rounded-2xl shadow-2xl border border-slate-700/50 overflow-hidden" onClick={e => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="p-6 text-center border-b border-slate-800">
|
||||
<div className="w-14 h-14 mx-auto mb-3 rounded-2xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center shadow-lg shadow-cyan-500/20">
|
||||
<svg className="w-7 h-7 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>
|
||||
<h3 className="text-lg font-bold text-white">Security Tier Configuration</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">Tenant: <strong className="text-white">{tierModal.tenantName}</strong></p>
|
||||
</div>
|
||||
|
||||
{/* Radio Options */}
|
||||
<div className="p-5 space-y-2">
|
||||
{Object.entries(TIER_CONFIG).map(([key, cfg]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setSelectedTier(key)}
|
||||
className={`w-full text-left p-4 rounded-xl border-2 transition-all ${
|
||||
selectedTier === key
|
||||
? `${cfg.bg} ${cfg.border} ring-1 ${cfg.border}`
|
||||
: 'border-slate-700/50 hover:border-slate-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedTier === key ? cfg.border : 'border-slate-600'
|
||||
}`}>
|
||||
{selectedTier === key && <div className={`w-2.5 h-2.5 rounded-full ${
|
||||
key === 'STANDARD' ? 'bg-emerald-400' : key === 'SOVEREIGN' ? 'bg-cyan-400' : 'bg-amber-400'
|
||||
}`}></div>}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">{cfg.icon}</span>
|
||||
<span className={`text-sm font-bold ${selectedTier === key ? cfg.color : 'text-slate-300'}`}>{cfg.label}</span>
|
||||
{tierModal.currentTier === key && <span className="text-[8px] font-bold px-1.5 py-0.5 rounded bg-slate-700 text-slate-400 uppercase">Current</span>}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 mt-0.5 ml-7">{cfg.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="px-5 pb-5 flex gap-3">
|
||||
<button
|
||||
onClick={() => setTierModal(null)}
|
||||
disabled={tierSwitching}
|
||||
className="flex-1 px-4 py-3 bg-slate-800 hover:bg-slate-700 text-slate-300 font-bold rounded-xl transition-colors border border-slate-700"
|
||||
>Batal</button>
|
||||
<button
|
||||
onClick={applySecurityTier}
|
||||
disabled={tierSwitching || selectedTier === tierModal.currentTier}
|
||||
className={`flex-1 px-4 py-3 font-bold rounded-xl transition-all shadow-lg flex items-center justify-center gap-2 ${
|
||||
tierSwitching || selectedTier === tierModal.currentTier
|
||||
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-cyan-600 to-blue-600 text-white hover:shadow-cyan-500/30 hover:scale-[1.02]'
|
||||
}`}
|
||||
>
|
||||
{tierSwitching ? (
|
||||
<><div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div> Switching...</>
|
||||
) : (
|
||||
<><svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> {selectedTier === tierModal.currentTier ? 'No Change' : 'Apply Security Tier'}</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function SovereignSetup() {
|
||||
const [os, setOs] = useState<string>("unknown");
|
||||
const [certStatus, setCertStatus] = useState<"idle" | "testing" | "ok" | "fail">("idle");
|
||||
const [tenantName, setTenantName] = useState("SOVEREIGN TENANT");
|
||||
|
||||
useEffect(() => {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (ua.includes("android")) setOs("android");
|
||||
else if (ua.includes("iphone") || ua.includes("ipad")) setOs("ios");
|
||||
else if (ua.includes("mac")) setOs("macos");
|
||||
else if (ua.includes("windows")) setOs("windows");
|
||||
else if (ua.includes("linux")) setOs("linux");
|
||||
else setOs("unknown");
|
||||
|
||||
// Fetch tenant info
|
||||
fetch("/api/auth/me").then(r => r.json()).then(d => {
|
||||
if (d.tenantName) setTenantName(d.tenantName);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const testConnection = async () => {
|
||||
setCertStatus("testing");
|
||||
try {
|
||||
const resp = await fetch("/api/superadmin/xcu-tls-switch");
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const onlineNodes = data.nodes?.filter((n: any) => n.online) || [];
|
||||
setCertStatus(onlineNodes.length > 0 ? "ok" : "fail");
|
||||
} else {
|
||||
setCertStatus("fail");
|
||||
}
|
||||
} catch {
|
||||
setCertStatus("fail");
|
||||
}
|
||||
};
|
||||
|
||||
const instructions: Record<string, { title: string; icon: string; steps: string[] }> = {
|
||||
android: {
|
||||
title: "Android",
|
||||
icon: "📱",
|
||||
steps: [
|
||||
"Tap tombol \"Download CA Certificate\" di bawah",
|
||||
"Buka Settings → Security → Encryption & Credentials",
|
||||
"Tap \"Install a certificate\" → \"CA certificate\"",
|
||||
"Pilih file xcu-sovereign-ca.crt yang baru di-download",
|
||||
"Konfirmasi dengan PIN/Fingerprint",
|
||||
"Selesai! Certificate berlaku 30 tahun"
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
title: "iPhone / iPad",
|
||||
icon: "🍎",
|
||||
steps: [
|
||||
"Tap tombol \"Download CA Certificate\" di bawah",
|
||||
"iOS akan menampilkan \"Profile Downloaded\"",
|
||||
"Buka Settings → General → VPN & Device Management",
|
||||
"Tap profile \"XCU Sovereign CA\" → Install",
|
||||
"Buka Settings → General → About → Certificate Trust Settings",
|
||||
"Enable trust untuk \"XCU Sovereign CA\"",
|
||||
"Selesai! Certificate berlaku 30 tahun"
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
title: "Windows",
|
||||
icon: "🖥️",
|
||||
steps: [
|
||||
"Klik tombol \"Download CA Certificate\" di bawah",
|
||||
"Double-click file xcu-sovereign-ca.crt",
|
||||
"Klik \"Install Certificate...\"",
|
||||
"Pilih \"Local Machine\" → Next",
|
||||
"Pilih \"Place all certificates in the following store\"",
|
||||
"Klik Browse → pilih \"Trusted Root Certification Authorities\"",
|
||||
"Klik Finish → Yes",
|
||||
"Restart browser. Selesai!"
|
||||
]
|
||||
},
|
||||
macos: {
|
||||
title: "macOS",
|
||||
icon: "💻",
|
||||
steps: [
|
||||
"Klik tombol \"Download CA Certificate\" di bawah",
|
||||
"Double-click file xcu-sovereign-ca.crt → Keychain Access terbuka",
|
||||
"Certificate muncul di login keychain",
|
||||
"Double-click certificate → Trust → \"Always Trust\"",
|
||||
"Tutup dialog, masukkan password Mac",
|
||||
"Restart browser. Selesai!"
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
title: "Linux",
|
||||
icon: "🐧",
|
||||
steps: [
|
||||
"Download CA Certificate",
|
||||
"sudo cp xcu-sovereign-ca.crt /usr/local/share/ca-certificates/",
|
||||
"sudo update-ca-certificates",
|
||||
"Restart browser. Selesai!"
|
||||
]
|
||||
},
|
||||
unknown: {
|
||||
title: "Device",
|
||||
icon: "🔧",
|
||||
steps: ["Download CA Certificate dan install sesuai OS Anda"]
|
||||
}
|
||||
};
|
||||
|
||||
const currentOs = instructions[os] || instructions.unknown;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white">
|
||||
{/* Header */}
|
||||
<header className="border-b border-slate-800 px-6 py-4">
|
||||
<div className="max-w-4xl mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center shadow-lg">
|
||||
<svg className="w-5 h-5 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>
|
||||
<h1 className="text-lg font-bold tracking-tight">Sovereign Security Setup</h1>
|
||||
<p className="text-xs text-slate-400">{tenantName} • Private CA Installation</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/supreme-admin" className="text-xs text-slate-400 hover:text-white transition-colors">← Back to Dashboard</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-4xl mx-auto px-6 py-10 space-y-8">
|
||||
{/* Hero Card */}
|
||||
<div className="bg-gradient-to-r from-amber-500/10 via-orange-500/5 to-transparent border border-amber-500/20 rounded-2xl p-8">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="text-4xl">🛡️</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-2">Mode Sovereign Aktif</h2>
|
||||
<p className="text-slate-300 text-sm leading-relaxed">
|
||||
Tenant ini beroperasi dalam <strong className="text-amber-400">Security Tier SOVEREIGN</strong>.
|
||||
Semua koneksi QUIC/WebTransport menggunakan Private CA yang tidak bergantung pada internet global.
|
||||
Setiap device perlu install sertifikat CA <strong>1× saja</strong>, berlaku <strong>30 tahun</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Download Section */}
|
||||
<div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-8">
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-emerald-500/20 flex items-center justify-center text-emerald-400 text-sm font-bold">1</span>
|
||||
Download Certificate
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<a
|
||||
href="/api/superadmin/sovereign-ca/download"
|
||||
download="xcu-sovereign-ca.crt"
|
||||
className="inline-flex items-center gap-3 px-6 py-3 bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-500 hover:to-emerald-400 text-white font-bold rounded-xl transition-all duration-300 shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/40 hover:scale-105"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
Download CA Certificate
|
||||
</a>
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||
<span>Format: X.509 PEM</span>
|
||||
<span>•</span>
|
||||
<span>Validitas: 30 tahun</span>
|
||||
<span>•</span>
|
||||
<span>Algorithm: RSA-4096 + SHA-256</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* OS-Specific Instructions */}
|
||||
<div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-8">
|
||||
<h3 className="text-lg font-bold mb-1 flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center text-blue-400 text-sm font-bold">2</span>
|
||||
Install Certificate
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mb-6">
|
||||
Terdeteksi: <strong className="text-cyan-400">{currentOs.icon} {currentOs.title}</strong>
|
||||
</p>
|
||||
|
||||
{/* OS Tabs */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{Object.entries(instructions).filter(([k]) => k !== 'unknown').map(([key, val]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setOs(key)}
|
||||
className={`px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${
|
||||
os === key
|
||||
? 'bg-cyan-500 text-white shadow-lg shadow-cyan-500/30'
|
||||
: 'bg-slate-700 text-slate-400 hover:bg-slate-600 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{val.icon} {val.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="space-y-3">
|
||||
{currentOs.steps.map((step, idx) => (
|
||||
<div key={idx} className="flex gap-3 items-start">
|
||||
<div className="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px] font-bold text-cyan-400 flex-shrink-0 mt-0.5">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<p className="text-sm text-slate-300">{step}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection Test */}
|
||||
<div className="bg-slate-800/50 border border-slate-700 rounded-2xl p-8">
|
||||
<h3 className="text-lg font-bold mb-4 flex items-center gap-2">
|
||||
<span className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center text-purple-400 text-sm font-bold">3</span>
|
||||
Verify Connection
|
||||
</h3>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={testConnection}
|
||||
disabled={certStatus === "testing"}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-600 to-purple-500 hover:from-purple-500 hover:to-purple-400 text-white font-bold rounded-xl transition-all duration-300 shadow-lg shadow-purple-500/20 disabled:opacity-50"
|
||||
>
|
||||
{certStatus === "testing" ? (
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
)}
|
||||
Test Connection
|
||||
</button>
|
||||
{certStatus === "ok" && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-emerald-500/10 border border-emerald-500/20 rounded-xl">
|
||||
<span className="relative flex h-3 w-3">
|
||||
<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-3 w-3 bg-emerald-400"></span>
|
||||
</span>
|
||||
<span className="text-sm font-bold text-emerald-400">Koneksi Aman — XCU Engine Online</span>
|
||||
</div>
|
||||
)}
|
||||
{certStatus === "fail" && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-red-500/10 border border-red-500/20 rounded-xl">
|
||||
<span className="w-3 h-3 rounded-full bg-red-500"></span>
|
||||
<span className="text-sm font-bold text-red-400">Koneksi Gagal — Pastikan certificate sudah terinstall</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="text-center text-xs text-slate-600 py-4">
|
||||
XCom ULTRA • Sovereign Security Infrastructure • Private CA • Zero Internet Dependency
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function PanopticonTelemetry() {
|
||||
const { t } = useDictionary();
|
||||
const [logs, setLogs] = useState<{ id: string; time: string; action: string; target: string; status: string }[]>([]);
|
||||
const [activeConnections, setActiveConnections] = useState(14029);
|
||||
const [bandwidth, setBandwidth] = useState(24.5);
|
||||
const [targetKillId, setTargetKillId] = useState("");
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const router = useRouter();
|
||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/superadmin/supreme-dashboard");
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
router.push("/");
|
||||
} else {
|
||||
setIsCheckingAuth(false);
|
||||
}
|
||||
} catch (err) {
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, [router]);
|
||||
|
||||
// Move conditional return AFTER all hooks to comply with React Rules of Hooks
|
||||
// (early return is placed after line 124 instead)
|
||||
|
||||
// WebGL-Style Waveform Simulation using Canvas for Zero-Error absolute performance
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = canvas.clientWidth;
|
||||
canvas.height = canvas.clientHeight;
|
||||
};
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const draw = () => {
|
||||
time += 0.05;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Draw multiple waves
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, centerY);
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const frequency = 0.01 + (i * 0.005);
|
||||
const amplitude = 30 + (i * 15) + Math.sin(time + x * 0.02) * 10;
|
||||
const y = Math.sin(x * frequency + time + i * 2) * amplitude;
|
||||
ctx.lineTo(x, centerY + y);
|
||||
}
|
||||
|
||||
// WhatsApp Emerald to Zoom Blue Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
gradient.addColorStop(0, "rgba(37, 211, 102, 0.4)"); // WhatsApp Green
|
||||
gradient.addColorStop(0.5, "rgba(11, 92, 255, 0.6)"); // Zoom Blue
|
||||
gradient.addColorStop(1, "rgba(255, 0, 128, 0.3)"); // XCU Magenta
|
||||
|
||||
ctx.strokeStyle = gradient;
|
||||
ctx.lineWidth = 2 + i;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch real telemetry data from PANOPTICON backend
|
||||
useEffect(() => {
|
||||
const fetchTelemetry = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/superadmin/telemetry");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Transform quantum logs into UI log format
|
||||
const formattedLogs = data.logs.map((log: any) => ({
|
||||
id: log.id,
|
||||
time: log.nanoTimestamp ? new Date(log.nanoTimestamp).toISOString().split("T")[1].substring(0, 8) : "00:00:00",
|
||||
action: log.action,
|
||||
target: log.targetId,
|
||||
status: log.action.includes("KILL") ? "TERMINATED" : "OK",
|
||||
}));
|
||||
setLogs(formattedLogs);
|
||||
|
||||
setActiveConnections(data.stats.totalTelemetryRecords);
|
||||
setBandwidth(parseFloat(data.stats.totalBandwidthBytes) / 1024 / 1024 / 1024);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("PANOPTICON Sync Error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTelemetry();
|
||||
const interval = setInterval(fetchTelemetry, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Auth guard AFTER all hooks (React Rules of Hooks compliance)
|
||||
if (isCheckingAuth) {
|
||||
return <div className="min-h-screen bg-black flex items-center justify-center text-purple-500 font-mono text-sm">INITIALIZING SECURE CONNECTION...</div>;
|
||||
}
|
||||
|
||||
const executeLiveKill = async () => {
|
||||
if (!targetKillId) return;
|
||||
|
||||
// Spectactular Kill Trigger Effect
|
||||
document.body.style.animation = "shake 0.5s cubic-bezier(.36,.07,.19,.97) both";
|
||||
setTimeout(() => document.body.style.animation = "", 500);
|
||||
|
||||
const res = await fetch("/api/telemetry/live-kill", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
targetType: "USER",
|
||||
targetId: targetKillId,
|
||||
reason: "SUPREME_ADMIN_OVERRIDE"
|
||||
})
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setLogs(prev => [{
|
||||
id: "KILL_" + Date.now(),
|
||||
time: new Date().toISOString().split("T")[1].substring(0, 8),
|
||||
action: "LIVE_KILL_EXECUTE",
|
||||
target: targetKillId,
|
||||
status: "TERMINATED"
|
||||
}, ...prev]);
|
||||
setTargetKillId("");
|
||||
alert("XCU MoQ Ejection Berhasil. Sesi Dihanguskan.");
|
||||
} else {
|
||||
alert("Akses Ditolak: Quantum Jurisdiction");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#050914] text-gray-200 font-sans selection:bg-blue-500/30 overflow-hidden relative">
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||
}
|
||||
.matrix-bg {
|
||||
background-image:
|
||||
linear-gradient(rgba(11, 92, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(37, 211, 102, 0.05) 1px, transparent 1px);
|
||||
background-size: 30px 30px;
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Decorative Grid */}
|
||||
<div className="absolute inset-0 matrix-bg pointer-events-none z-0"></div>
|
||||
|
||||
<div className="relative z-10 p-6 md:p-10 max-w-7xl mx-auto flex flex-col gap-8 h-screen">
|
||||
|
||||
{/* HEADER */}
|
||||
<header className="flex justify-between items-center border-b border-white/10 pb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-[#25D366] via-[#0b5cff] to-[#ff0080]">
|
||||
PANOPTICON OMNISCIENT
|
||||
</h1>
|
||||
<p className="text-sm font-mono text-blue-400/80 uppercase tracking-widest mt-1">
|
||||
Supreme Admin XCU • Level 1 Akses Absolut
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="px-4 py-2 rounded-lg bg-green-500/10 border border-green-500/30 flex items-center gap-2 shadow-[0_0_15px_rgba(37,211,102,0.2)]">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
|
||||
<span className="font-mono text-green-400 text-sm font-bold">CLUSTER STABLE</span>
|
||||
</div>
|
||||
<a href="/supreme-admin" className="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-sm font-bold transition-all">
|
||||
Tutup Dasbor
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* TOP METRICS */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">XCU Active MoC Streams</p>
|
||||
<p className="text-5xl font-black mt-2 text-white font-mono tracking-tighter">
|
||||
{activeConnections.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl p-6 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-green-500/10 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500"></div>
|
||||
<p className="text-sm font-mono text-gray-500 uppercase">Global Bandwidth (Gbps)</p>
|
||||
<p className="text-5xl font-black mt-2 text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-500 font-mono tracking-tighter">
|
||||
{bandwidth.toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 backdrop-blur-xl border border-red-500/20 rounded-2xl p-6 relative shadow-[0_0_30px_rgba(255,0,0,0.1)]">
|
||||
<p className="text-sm font-bold text-red-500 uppercase mb-4 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L1 21h22M12 6l7.5 13h-15M11 10h2v5h-2M11 16h2v2h-2"/></svg>
|
||||
GUILLOTINE PANEL
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={targetKillId}
|
||||
onChange={(e) => setTargetKillId(e.target.value)}
|
||||
placeholder="Target ID (e.g. tsm@pc24.id)"
|
||||
className="w-full bg-black/50 border border-red-500/30 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-red-500 text-white placeholder-gray-600"
|
||||
/>
|
||||
<button
|
||||
onClick={executeLiveKill}
|
||||
className="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm tracking-widest shadow-[0_0_15px_rgba(255,0,0,0.4)] transition-all hover:scale-105 active:scale-95"
|
||||
>
|
||||
KILL
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VISUALIZATION & LOGS */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 flex-1 min-h-0">
|
||||
|
||||
{/* Waveform Canvas */}
|
||||
<div className="lg:col-span-2 bg-black/40 backdrop-blur-xl border border-white/5 rounded-2xl relative overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-white/5 flex justify-between items-center bg-black/40">
|
||||
<p className="text-xs font-mono text-blue-400">LIVE XCU eBPF TELEMETRY (ZOOM x WA MULTIPLEXING)</p>
|
||||
<div className="flex gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 relative">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-80 mix-blend-screen"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hacker Log Stream */}
|
||||
<div className="bg-black/60 backdrop-blur-xl border border-white/5 rounded-2xl flex flex-col overflow-hidden relative shadow-inner">
|
||||
<div className="absolute inset-x-0 top-0 h-8 bg-gradient-to-b from-blue-500/10 to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="p-4 border-b border-white/5 bg-[#0a0a0a]">
|
||||
<p className="text-xs font-mono text-gray-400 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-ping"></span>
|
||||
QUANTUM TRACE LOGS
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4 font-mono text-xs flex flex-col gap-1">
|
||||
{logs.map((log, i) => (
|
||||
<div key={log.id + i} className="flex gap-3 hover:bg-white/5 px-2 py-1 rounded transition-colors group">
|
||||
<span className="text-gray-600">[{log.time}]</span>
|
||||
<span className={log.status === 'TERMINATED' ? 'text-red-500 font-bold' : log.status === 'WARN' ? 'text-yellow-500' : 'text-green-400'}>
|
||||
{log.action}
|
||||
</span>
|
||||
<span className="text-blue-300 ml-auto opacity-70 group-hover:opacity-100">{log.target}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useDictionary } from "@/lib/dictionary";
|
||||
import { useOmni } from "@/components/OmniSyncProvider";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface NeuralSubject {
|
||||
id: string;
|
||||
name: string;
|
||||
attentionScore: number;
|
||||
cognitiveLoad: number;
|
||||
alphaWave: number;
|
||||
status: "FOCUSED" | "DISTRACTED" | "ZONED_OUT";
|
||||
}
|
||||
|
||||
export default function TelepathyMatrix() {
|
||||
const { t } = useDictionary();
|
||||
const { theme, locale } = useOmni();
|
||||
const router = useRouter();
|
||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||
|
||||
// ALL HOOKS MUST BE DECLARED BEFORE ANY CONDITIONAL RETURN (React Rules of Hooks)
|
||||
const [subjects, setSubjects] = useState<NeuralSubject[]>([
|
||||
{ id: "xcu_subj_01", name: "Commander Alpha", attentionScore: 95, cognitiveLoad: 80, alphaWave: 0.8, status: "FOCUSED" },
|
||||
{ id: "xcu_subj_02", name: "Operative Bravo", attentionScore: 88, cognitiveLoad: 60, alphaWave: 0.6, status: "FOCUSED" },
|
||||
{ id: "xcu_subj_03", name: "Spectre 7", attentionScore: 45, cognitiveLoad: 30, alphaWave: 0.4, status: "DISTRACTED" },
|
||||
{ id: "xcu_subj_04", name: "Agent Echo", attentionScore: 12, cognitiveLoad: 10, alphaWave: 0.1, status: "ZONED_OUT" },
|
||||
{ id: "xcu_subj_05", name: "Node Delta", attentionScore: 78, cognitiveLoad: 70, alphaWave: 0.7, status: "FOCUSED" },
|
||||
{ id: "xcu_subj_06", name: "Vanguard 1", attentionScore: 25, cognitiveLoad: 15, alphaWave: 0.2, status: "ZONED_OUT" },
|
||||
]);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/superadmin/supreme-dashboard");
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
router.push("/");
|
||||
} else {
|
||||
setIsCheckingAuth(false);
|
||||
}
|
||||
} catch (err) {
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, [router]);
|
||||
|
||||
// Brainwave Visualization Effect - MUST be before conditional return (React Rules of Hooks)
|
||||
useEffect(() => {
|
||||
if (isCheckingAuth) return;
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number;
|
||||
let time = 0;
|
||||
|
||||
const resize = () => {
|
||||
canvas.width = canvas.parentElement?.clientWidth || 800;
|
||||
canvas.height = 150;
|
||||
};
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
|
||||
const draw = () => {
|
||||
time += 0.05;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
const centerY = height / 2;
|
||||
|
||||
// Draw 3 Brainwaves (Alpha, Beta, Gamma)
|
||||
const waves = [
|
||||
{ color: "rgba(11, 92, 255, 0.8)", freq: 0.02, amp: 20, speed: 1.5 }, // Alpha (Blue)
|
||||
{ color: "rgba(37, 211, 102, 0.8)", freq: 0.05, amp: 10, speed: 2.5 }, // Beta (Green)
|
||||
{ color: "rgba(255, 0, 128, 0.8)", freq: 0.1, amp: 5, speed: 4.0 } // Gamma (Magenta)
|
||||
];
|
||||
|
||||
waves.forEach((wave, i) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, centerY);
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const y = Math.sin(x * wave.freq + time * wave.speed + i) * wave.amp
|
||||
+ Math.sin(x * wave.freq * 0.5 + time) * (wave.amp * 0.5);
|
||||
ctx.lineTo(x, centerY + y);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = wave.color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
});
|
||||
|
||||
// Draw grid overlay
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.05)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let x = 0; x < width; x += 30) {
|
||||
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke();
|
||||
}
|
||||
for (let y = 0; y < height; y += 30) {
|
||||
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke();
|
||||
}
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, [isCheckingAuth]);
|
||||
|
||||
// Simulate real-time neural data updates - MUST be before conditional return
|
||||
useEffect(() => {
|
||||
if (isCheckingAuth) return;
|
||||
const interval = setInterval(() => {
|
||||
setSubjects(prev => prev.map(s => {
|
||||
let newScore = s.attentionScore + (Math.random() * 10 - 5);
|
||||
newScore = Math.max(0, Math.min(100, newScore));
|
||||
|
||||
let newStatus: "FOCUSED" | "DISTRACTED" | "ZONED_OUT" = "FOCUSED";
|
||||
if (newScore < 30) newStatus = "ZONED_OUT";
|
||||
else if (newScore < 60) newStatus = "DISTRACTED";
|
||||
|
||||
return {
|
||||
...s,
|
||||
attentionScore: newScore,
|
||||
cognitiveLoad: Math.max(0, Math.min(100, s.cognitiveLoad + (Math.random() * 6 - 3))),
|
||||
status: newStatus
|
||||
};
|
||||
}));
|
||||
}, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [isCheckingAuth]);
|
||||
|
||||
if (isCheckingAuth) {
|
||||
return <div className="min-h-screen bg-black flex items-center justify-center text-purple-500 font-mono text-sm">INITIALIZING SECURE CONNECTION...</div>;
|
||||
}
|
||||
|
||||
const sendSensoryPing = (id: string, name: string) => {
|
||||
// Flash the screen to simulate ping
|
||||
const el = document.getElementById(`card-${id}`);
|
||||
if (el) {
|
||||
el.classList.add("ring-4", "ring-[#ff0080]", "ring-opacity-100", "scale-105");
|
||||
setTimeout(() => el.classList.remove("ring-4", "ring-[#ff0080]", "ring-opacity-100", "scale-105"), 500);
|
||||
}
|
||||
alert(`Sensory Ping (Haptic & Audio) disuntikkan langsung ke korteks partisipan [${name}]`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen ${theme === 'dark' ? 'bg-[#03060a] text-gray-200' : 'bg-gray-50 text-gray-900'} font-sans selection:bg-purple-500/30 overflow-x-hidden relative`}>
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes cyber-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.8; transform: scale(0.98); }
|
||||
}
|
||||
.animate-cyber-pulse {
|
||||
animation: cyber-pulse 2s infinite ease-in-out;
|
||||
}
|
||||
@keyframes emergency-flash {
|
||||
0%, 100% { background-color: rgba(220, 38, 38, 0.1); border-color: rgba(220, 38, 38, 0.3); }
|
||||
50% { background-color: rgba(220, 38, 38, 0.4); border-color: rgba(220, 38, 38, 0.8); box-shadow: 0 0 20px rgba(220, 38, 38, 0.6); }
|
||||
}
|
||||
.emergency-flash {
|
||||
animation: emergency-flash 1s infinite;
|
||||
}
|
||||
`}} />
|
||||
|
||||
{/* Decorative Grid */}
|
||||
<div className="absolute inset-0 z-0 pointer-events-none" style={{
|
||||
backgroundImage: theme === 'dark'
|
||||
? 'linear-gradient(rgba(255, 0, 128, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 0, 128, 0.03) 1px, transparent 1px)'
|
||||
: 'linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px'
|
||||
}}></div>
|
||||
|
||||
<div className="relative z-10 p-6 md:p-10 max-w-7xl mx-auto flex flex-col gap-8">
|
||||
|
||||
{/* HEADER */}
|
||||
<header className="flex flex-col md:flex-row justify-between items-start md:items-center border-b border-white/10 pb-6 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-8 h-8 bg-purple-500/20 rounded-lg flex items-center justify-center border border-purple-500/50 shadow-[0_0_15px_rgba(168,85,247,0.4)]">
|
||||
<svg className="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-[#ff0080] to-[#7928ca]">
|
||||
TELEPATHY MATRIX
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-sm font-mono text-purple-400/80 uppercase tracking-widest">
|
||||
Global Neural Attention & Cognitive Load Dashboard
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="px-4 py-2 rounded-lg bg-red-500/10 border border-red-500/30 flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>
|
||||
<span className="font-mono text-red-400 text-xs font-bold uppercase">Surveillance Active</span>
|
||||
</div>
|
||||
<Link href="/supreme-admin" className="px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 text-sm font-bold transition-all shadow-sm">
|
||||
Kembali ke Supreme Admin
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* MASTER OSCILLATOR CANVAS */}
|
||||
<div className={`rounded-2xl border ${theme === 'dark' ? 'bg-black/60 border-white/10' : 'bg-white border-gray-200'} backdrop-blur-xl p-6 relative overflow-hidden shadow-2xl`}>
|
||||
<div className="absolute top-4 left-4 flex items-center gap-2 z-20">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-gray-400">Aggregated Brainwaves</span>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 flex gap-3 z-20 text-[9px] font-black uppercase tracking-widest">
|
||||
<div className="flex items-center gap-1"><span className="w-2 h-2 bg-blue-500 rounded-full"></span> Alpha (Relaxed)</div>
|
||||
<div className="flex items-center gap-1"><span className="w-2 h-2 bg-green-500 rounded-full"></span> Beta (Focused)</div>
|
||||
<div className="flex items-center gap-1"><span className="w-2 h-2 bg-[#ff0080] rounded-full"></span> Gamma (High Load)</div>
|
||||
</div>
|
||||
<div className="w-full h-[150px] mt-4 relative z-10 opacity-80 mix-blend-screen">
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NEURAL GRID */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-black uppercase tracking-widest flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"></path><path fillRule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clipRule="evenodd"></path></svg>
|
||||
Active Subjects
|
||||
</h2>
|
||||
<div className="text-[10px] font-mono bg-white/5 border border-white/10 px-3 py-1 rounded-full text-gray-400">
|
||||
{subjects.filter(s => s.status === 'ZONED_OUT').length} CRITICAL / {subjects.length} TOTAL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{subjects.map(subject => {
|
||||
const isCritical = subject.status === 'ZONED_OUT';
|
||||
return (
|
||||
<div
|
||||
id={`card-${subject.id}`}
|
||||
key={subject.id}
|
||||
className={`glass-panel p-6 rounded-2xl border transition-all duration-300 relative overflow-hidden group ${
|
||||
isCritical ? 'emergency-flash' : theme === 'dark' ? 'border-white/5 bg-white/2 hover:border-white/20' : 'border-gray-200 bg-white hover:border-purple-300 shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{/* Background Glow */}
|
||||
<div className={`absolute -right-10 -top-10 w-32 h-32 rounded-full blur-3xl opacity-20 pointer-events-none transition-colors ${
|
||||
isCritical ? 'bg-red-500' : subject.status === 'FOCUSED' ? 'bg-emerald-500' : 'bg-amber-500'
|
||||
}`}></div>
|
||||
|
||||
<div className="flex justify-between items-start mb-6 relative z-10">
|
||||
<div>
|
||||
<div className="text-[10px] font-mono text-gray-500">{subject.id}</div>
|
||||
<h3 className={`text-lg font-black tracking-tight mt-1 ${theme === 'dark' ? 'text-white' : 'text-gray-900'}`}>{subject.name}</h3>
|
||||
</div>
|
||||
<div className={`px-2 py-1 rounded-md text-[9px] font-black uppercase tracking-widest border ${
|
||||
isCritical ? 'bg-red-500/20 text-red-500 border-red-500/30' :
|
||||
subject.status === 'FOCUSED' ? 'bg-emerald-500/20 text-emerald-500 border-emerald-500/30' :
|
||||
'bg-amber-500/20 text-amber-500 border-amber-500/30'
|
||||
}`}>
|
||||
{subject.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 relative z-10">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1 font-bold">
|
||||
<span className="text-gray-500 uppercase">Attention Score</span>
|
||||
<span className={isCritical ? 'text-red-500' : 'text-emerald-500'}>{subject.attentionScore.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-black/40 rounded-full h-1.5 overflow-hidden border border-white/5">
|
||||
<div className={`h-full transition-all duration-500 ${isCritical ? 'bg-red-500' : 'bg-emerald-500'}`} style={{ width: `${subject.attentionScore}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-xs mb-1 font-bold">
|
||||
<span className="text-gray-500 uppercase">Cognitive Load</span>
|
||||
<span className="text-purple-400">{subject.cognitiveLoad.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-black/40 rounded-full h-1.5 overflow-hidden border border-white/5">
|
||||
<div className="bg-purple-500 h-full transition-all duration-500" style={{ width: `${subject.cognitiveLoad}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-white/10 relative z-10 flex justify-between items-center">
|
||||
<div className="text-[9px] font-mono text-gray-500">
|
||||
Alpha: {subject.alphaWave.toFixed(2)}Hz
|
||||
</div>
|
||||
<button
|
||||
onClick={() => sendSensoryPing(subject.id, subject.name)}
|
||||
className={`px-4 py-2 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all shadow-lg hover:scale-105 active:scale-95 ${
|
||||
isCritical
|
||||
? 'bg-red-600 hover:bg-red-500 text-white shadow-red-500/30'
|
||||
: 'bg-white/10 hover:bg-white/20 text-gray-300 border border-white/10'
|
||||
}`}
|
||||
>
|
||||
Sensory Ping
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function TestPage() {
|
||||
return <div>TEST PAGE WORKS</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user