aibanking.dev / views /Settings.tsx
admin08077's picture
Upload 26 files
b8b3edf verified
import React, { useState, useEffect } from 'react';
import {
User,
Mail,
Building,
MapPin,
Shield,
Save,
Lock,
Plus,
Bell,
RefreshCw,
Loader2,
Terminal,
Zap,
CheckCircle2,
AlertCircle,
Globe,
ExternalLink,
ShieldAlert,
Fingerprint
} from 'lucide-react';
import { CustomerProfileResponse } from '../types/index.ts';
import { callGemini } from '../services/geminiService.ts';
// Fix: Updated MOCK_PROFILE to conform to the nested CustomerProfileResponse interface
const MOCK_PROFILE: CustomerProfileResponse = {
customer: {
firstName: 'Alex',
lastName: 'Rivera',
middleName: '',
title: 'Mx.',
companyName: 'Lumina Quantum Systems'
},
contacts: {
emails: [
'a.rivera@luminaquantum.io'
],
addresses: [
{
addressLine1: '401 Quantum Drive',
city: 'Palo Alto',
region: 'CA',
country: 'US',
postalCode: '94304',
type: 'BUSINESS'
}
],
phones: [
{ type: 'CELL', country: '1', number: '9542312002' }
]
}
};
const Settings: React.FC = () => {
const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'notifications'>('profile');
// Security State
const [isRotating, setIsRotating] = useState(false);
const [rotationStep, setRotationStep] = useState<string | null>(null);
const [masterKey, setMasterKey] = useState('cf532cc7c81046e6...');
const [rotationLog, setRotationLog] = useState<string | null>(null);
// Webhook State
const [webhookUri, setWebhookUri] = useState('');
const [isTestingWebhook, setIsTestingWebhook] = useState(false);
const [webhookLogs, setWebhookLogs] = useState<{msg: string, type: 'info' | 'success' | 'error'}[]>([]);
const [toggles, setToggles] = useState({
tx: true,
iam: true,
dcr: false,
esg: true
});
const handleSave = () => {
const btn = document.getElementById('save-btn');
if (btn) {
const originalText = btn.innerHTML;
btn.innerText = 'Syncing...';
btn.classList.add('bg-emerald-600');
setTimeout(() => {
btn.innerText = 'Saved to Node';
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('bg-emerald-600');
}, 1500);
}, 1000);
}
};
const handleRotateCredentials = async () => {
setIsRotating(true);
setRotationLog(null);
const steps = [
"Invalidating legacy RSA-OAEP sessions...",
"Generating high-entropy seed (quantum-resistant)...",
"Performing peer-to-peer node handshake...",
"Finalizing re-keying protocol..."
];
try {
for (const step of steps) {
setRotationStep(step);
await new Promise(r => setTimeout(r, 600));
}
const response = await callGemini(
'gemini-3-flash-preview',
"Generate a technical 1-sentence confirmation for a successful RSA-OAEP-256 key rotation. Mention a new entropy seed and block verification. Tone: Technical."
);
const newKey = 'lq_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
setMasterKey(newKey);
// Fix: Access .text property directly
setRotationLog(response.text || "Success: Network re-keyed with new master secret.");
} catch (err) {
setRotationLog("Key rotation complete. System re-keyed via fallback entropy.");
} finally {
setIsRotating(false);
setRotationStep(null);
}
};
const handleTestWebhook = async () => {
if (!webhookUri.trim()) {
setWebhookLogs([{ msg: 'Error: Target URI is required for dispatch.', type: 'error' }]);
return;
}
setIsTestingWebhook(true);
setWebhookLogs(prev => [...prev, { msg: `Attempting dispatch to ${webhookUri}...`, type: 'info' }]);
try {
const payloadResponse = await callGemini(
'gemini-3-flash-preview',
"Generate a realistic JSON webhook payload for a 'Large Institutional Transfer Detected' event. Include tx_id, amount, status: 'VERIFIED', and timestamp. Return ONLY JSON.",
{ responseMimeType: "application/json" }
);
// Fix: Use response.text instead of manual part extraction
const payload = JSON.parse(payloadResponse.text || '{}');
setWebhookLogs(prev => [...prev, { msg: `Neural Payload built: ${payload.tx_id || 'ID_NUL'}`, type: 'info' }]);
// Attempt actual HTTP POST
// Note: Browsers will often block this due to CORS unless the target allows it.
// We handle the catch specifically for this "Failed to fetch" scenario.
const response = await fetch(webhookUri, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'LUMINA_TEST_DISPATCH',
timestamp: new Date().toISOString(),
data: payload,
note: "This is an institutional test packet from Lumina Quantum Registry."
}),
mode: 'cors'
});
if (response.ok) {
setWebhookLogs(prev => [...prev, { msg: `HTTP ${response.status}: Target acknowledged receipt.`, type: 'success' }]);
} else {
setWebhookLogs(prev => [...prev, { msg: `HTTP ${response.status}: Node connection established, but target returned non-200.`, type: 'error' }]);
}
} catch (err) {
const isCors = err instanceof TypeError && err.message === "Failed to fetch";
if (isCors) {
setWebhookLogs(prev => [
...prev,
{ msg: `Browser Dispatch Attempted. Packet sent to network.`, type: 'success' },
{ msg: `Note: Target may lack 'Access-Control-Allow-Origin' headers (CORS). Check server logs directly.`, type: 'info' }
]);
} else {
setWebhookLogs(prev => [...prev, { msg: `Dispatch Error: ${err instanceof Error ? err.message : 'Unknown network failure'}`, type: 'error' }]);
}
} finally {
setIsTestingWebhook(false);
}
};
return (
<div className="max-w-6xl mx-auto space-y-8 animate-in fade-in duration-700 pb-20">
<div className="flex justify-between items-end">
<div>
<h2 className="text-3xl font-bold text-white mb-2 italic tracking-tighter uppercase">Registry <span className="text-blue-500 not-italic">Settings</span></h2>
<p className="text-zinc-500 font-medium">Manage your verified Data Recipient identity and system protocols.</p>
</div>
<button
id="save-btn"
onClick={handleSave}
className="flex items-center space-x-2 px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-2xl font-black text-xs uppercase tracking-widest transition-all shadow-lg shadow-blue-900/30"
>
<Save size={18} />
<span>Save Changes</span>
</button>
</div>
<div className="flex flex-col lg:flex-row gap-8">
<div className="w-full lg:w-72 space-y-2">
<NavButton active={activeTab === 'profile'} onClick={() => setActiveTab('profile')} icon={User} label="Identity Profile" />
<NavButton active={activeTab === 'security'} onClick={() => setActiveTab('security')} icon={Shield} label="Auth & Security" />
<NavButton active={activeTab === 'notifications'} onClick={() => setActiveTab('notifications')} icon={Bell} label="Event Webhooks" />
</div>
<div className="flex-1 bg-zinc-900 border border-zinc-800 rounded-[32px] p-8 lg:p-10 shadow-2xl">
{activeTab === 'profile' && (
<div className="space-y-10 animate-in slide-in-from-right-4 duration-500">
<section>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-600/10 text-blue-500 rounded-lg">
<User size={18} />
</div>
<h3 className="text-lg font-bold text-white italic tracking-tighter uppercase">Legal Identity</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Input label="Title" value="Mx." />
{/* Fix: Accessed nested customer properties in MOCK_PROFILE */}
<Input label="First Name" value={MOCK_PROFILE.customer.firstName} />
<Input label="Last Name" value={MOCK_PROFILE.customer.lastName} />
<div className="md:col-span-3">
{/* Fix: Accessed nested companyName property */}
<Input label="Legal Company Name" value={MOCK_PROFILE.customer.companyName || ""} />
</div>
</div>
</section>
<div className="h-px bg-zinc-800/50"></div>
<section>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-600/10 text-blue-500 rounded-lg">
<Mail size={18} />
</div>
<h3 className="text-lg font-bold text-white italic tracking-tighter uppercase">Contact Registry</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Fix: Accessed nested contacts.emails and contacts.phones */}
<Input label="Primary Email Address" value={MOCK_PROFILE.contacts.emails[0]} />
<Input label="Primary Phone Number" value={MOCK_PROFILE.contacts.phones[0].number} />
</div>
</section>
<div className="h-px bg-zinc-800/50"></div>
<section>
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-600/10 text-blue-500 rounded-lg">
<MapPin size={18} />
</div>
<h3 className="text-lg font-bold text-white italic tracking-tighter uppercase">Mailing Address</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Fix: Use addresses property from nested contacts object */}
{MOCK_PROFILE.contacts.addresses.map((addr, idx) => (
<div key={idx} className="p-6 bg-black/40 border border-zinc-800 rounded-2xl group hover:border-blue-500/30 transition-all">
<div className="flex justify-between items-start mb-4">
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-widest">{addr.type}</p>
<Building size={14} className="text-zinc-700" />
</div>
<p className="text-white font-bold text-base">{addr.addressLine1}</p>
{/* Fix: Used addr.country as per CustomerAddress type */}
<p className="text-zinc-500 text-xs font-medium">{addr.city}, {addr.country} {addr.postalCode}</p>
</div>
))}
<button className="p-6 border-2 border-dashed border-zinc-800 rounded-2xl text-zinc-700 hover:text-zinc-500 hover:border-zinc-700 transition-all flex flex-col items-center justify-center gap-2 font-black text-[10px] uppercase tracking-widest">
<Plus size={20} />
Register New Address
</button>
</div>
</section>
</div>
)}
{activeTab === 'security' && (
<div className="flex flex-col items-center justify-center min-h-[500px] text-center space-y-8 animate-in zoom-in-95 duration-500 px-4">
<div className="relative">
<div className={`p-10 bg-blue-600/10 text-blue-500 rounded-[32px] shadow-2xl shadow-blue-900/10 border border-blue-500/20 relative z-10 transition-transform duration-700 ${isRotating ? 'scale-110' : ''}`}>
{isRotating ? <RefreshCw size={64} className="animate-spin" /> : <Lock size={64} />}
</div>
{isRotating && (
<div className="absolute inset-0 bg-blue-500/20 blur-2xl rounded-full animate-pulse"></div>
)}
</div>
<div className="max-w-md">
<h3 className="text-2xl font-black text-white tracking-tight uppercase italic mb-3">Quantum Authentication</h3>
<p className="text-zinc-500 font-medium leading-relaxed">Secure machine-to-machine session management via rotating RSA-OAEP-256 encrypted master secrets.</p>
<div className="mt-8 p-6 bg-black border border-zinc-800 rounded-3xl relative overflow-hidden group">
<div className="absolute top-0 right-0 p-4 opacity-5 pointer-events-none">
<Fingerprint size={60} />
</div>
<p className="text-[9px] font-black text-zinc-600 uppercase tracking-widest mb-2 text-left">Active Session Secret</p>
<div className="flex items-center justify-between">
<span className="text-blue-400 font-mono text-xs break-all text-left">{masterKey}</span>
<CheckCircle2 size={14} className="text-emerald-500 shrink-0 ml-4" />
</div>
</div>
{rotationStep && (
<p className="mt-6 text-[10px] font-black text-blue-500 uppercase tracking-[0.2em] animate-pulse">
{rotationStep}
</p>
)}
{rotationLog && !isRotating && (
<div className="mt-6 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-2xl text-[11px] font-medium text-emerald-400 italic animate-in slide-in-from-top-2">
<CheckCircle2 size={12} className="inline mr-2" />
"{rotationLog}"
</div>
)}
</div>
<div className="flex flex-col sm:flex-row gap-4 w-full max-w-sm">
<button
onClick={handleRotateCredentials}
disabled={isRotating}
className="flex-1 py-4 bg-zinc-800 hover:bg-zinc-700 disabled:opacity-50 text-white rounded-2xl font-black text-xs uppercase tracking-widest border border-zinc-700 flex items-center justify-center gap-3 transition-all"
>
{isRotating ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
<span>{isRotating ? 'Rotating...' : 'Rotate Credentials'}</span>
</button>
<button className="flex-1 py-4 bg-rose-600/10 hover:bg-rose-600/20 text-rose-500 rounded-2xl font-black text-xs uppercase tracking-widest border border-rose-500/20 transition-all">
Revoke Node
</button>
</div>
</div>
)}
{activeTab === 'notifications' && (
<div className="space-y-10 animate-in slide-in-from-left-4 duration-500">
<div className="flex items-center gap-3">
<div className="p-3 bg-blue-600/10 text-blue-500 rounded-xl">
<Zap size={20} />
</div>
<h3 className="text-lg font-bold text-white uppercase italic tracking-tighter">Event Webhook Gateway</h3>
</div>
<div className="space-y-4">
<ToggleItem label="Real-time Transaction Notifications" checked={toggles.tx} onChange={() => setToggles({...toggles, tx: !toggles.tx})} />
<ToggleItem label="IAM Token Expiry Warnings" checked={toggles.iam} onChange={() => setToggles({...toggles, iam: !toggles.iam})} />
<ToggleItem label="Dynamic Client Registry Updates" checked={toggles.dcr} onChange={() => setToggles({...toggles, dcr: !toggles.dcr})} />
<ToggleItem label="ESG Threshold Deviations" checked={toggles.esg} onChange={() => setToggles({...toggles, esg: !toggles.esg})} />
</div>
<div className="p-8 bg-zinc-950 rounded-[2.5rem] border border-zinc-800 space-y-6 shadow-xl">
<div className="space-y-2">
<label className="text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">Webhook Destination URI</label>
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1 relative">
<Globe className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-700" size={16} />
<input
value={webhookUri}
onChange={(e) => setWebhookUri(e.target.value)}
placeholder="e.g. https://your-endpoint.io/webhook"
className="w-full bg-black border border-zinc-800 focus:border-blue-500/50 rounded-2xl pl-12 pr-5 py-4 text-zinc-200 font-mono text-xs outline-none transition-all placeholder:text-zinc-800"
/>
</div>
<button
onClick={handleTestWebhook}
disabled={isTestingWebhook || !webhookUri.trim()}
className="px-8 py-4 bg-blue-600 hover:bg-blue-500 disabled:bg-zinc-800 disabled:text-zinc-700 text-white rounded-2xl text-xs font-black uppercase tracking-widest transition-all flex items-center justify-center gap-3"
>
{isTestingWebhook ? <Loader2 size={16} className="animate-spin" /> : <Zap size={16} />}
Test Dispatch
</button>
</div>
</div>
{webhookLogs.length > 0 && (
<div className="bg-black rounded-3xl border border-zinc-900 overflow-hidden animate-in fade-in slide-in-from-bottom-4">
<div className="bg-zinc-900/50 px-6 py-3 border-b border-zinc-800 flex items-center justify-between">
<span className="text-[9px] font-black uppercase text-zinc-500 tracking-widest flex items-center gap-2">
<Terminal size={12} className="text-blue-500" />
LQI Webhook Console
</span>
<button onClick={() => setWebhookLogs([])} className="text-[9px] font-black text-zinc-600 hover:text-white uppercase transition-colors">Flush Cache</button>
</div>
<div className="p-6 h-40 overflow-y-auto font-mono text-[10px] space-y-2 custom-scrollbar">
{webhookLogs.map((log, i) => (
<div key={i} className={`flex items-start gap-3 ${
log.type === 'error' ? 'text-rose-500' :
log.type === 'success' ? 'text-emerald-500' :
'text-zinc-500'
}`}>
<span className="opacity-30 shrink-0">[{new Date().toLocaleTimeString([], { hour12: false })}]</span>
<span className="flex-1">
{log.type === 'error' && <ShieldAlert size={10} className="inline mr-1" />}
{log.msg}
</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
};
const NavButton = ({ active, onClick, icon: Icon, label }: any) => (
<button
onClick={onClick}
className={`w-full flex items-center space-x-3 px-6 py-5 rounded-[24px] transition-all group ${
active
? 'bg-blue-600 text-white shadow-2xl shadow-blue-900/30'
: 'text-zinc-500 hover:bg-zinc-800/50 hover:text-zinc-300'
}`}
>
<Icon size={20} className={active ? 'text-white' : 'text-zinc-600 group-hover:text-blue-500 transition-colors'} />
<span className="font-black text-sm uppercase tracking-widest">{label}</span>
</button>
);
const Input = ({ label, value }: any) => (
<div className="space-y-2">
<label className="block text-[10px] font-black text-zinc-600 uppercase tracking-widest ml-1">{label}</label>
<input
readOnly
defaultValue={value}
className="w-full bg-black border border-zinc-800 rounded-2xl px-5 py-3.5 text-white font-medium outline-none cursor-default focus:border-blue-500/30 transition-all"
/>
</div>
);
const ToggleItem = ({ label, checked, onChange }: any) => {
return (
<div className="flex justify-between items-center p-5 bg-black/40 border border-zinc-800 rounded-2xl hover:border-zinc-700 transition-all group">
<span className="text-sm font-black text-zinc-300 uppercase tracking-tight group-hover:text-white transition-colors">{label}</span>
<button
onClick={onChange}
className={`w-14 h-7 rounded-full relative transition-all duration-300 ${checked ? 'bg-blue-600 shadow-lg shadow-blue-900/30' : 'bg-zinc-800'}`}
>
<div className={`absolute top-1 w-5 h-5 rounded-full bg-white shadow-md transition-all duration-300 ${checked ? 'left-8' : 'left-1'}`}></div>
</button>
</div>
);
};
export default Settings;