332 lines
11 KiB
React
332 lines
11 KiB
React
import React, { useState } from 'react';
|
|
import {
|
|
ShieldCheck,
|
|
Lock,
|
|
LayoutGrid,
|
|
Activity,
|
|
Key,
|
|
ShieldAlert,
|
|
CheckCircle,
|
|
MailCheck,
|
|
User,
|
|
Sliders,
|
|
FileText,
|
|
Copy,
|
|
ExternalLink,
|
|
Info,
|
|
} from 'lucide-react';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { DashboardLayout } from '../components/layout/DashboardLayout';
|
|
import { Badge } from '../components/common/Badge';
|
|
import { Button } from '../components/common/Button';
|
|
import { Card } from '../components/common/Card';
|
|
import { MOCK_STATS, MOCK_ACTIVITIES } from '../services/mockData';
|
|
import '../styles/dashboard.css';
|
|
|
|
export function Dashboard() {
|
|
const { user, token, isDemoMode, logout } = useAuth();
|
|
const [activeTab, setActiveTab] = useState('dashboard');
|
|
const [showTokenModal, setShowTokenModal] = useState(false);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
// Icon mapping for stats
|
|
const statIcons = {
|
|
ShieldCheck,
|
|
Lock,
|
|
LayoutGrid,
|
|
Activity,
|
|
};
|
|
|
|
// Icon mapping for activities
|
|
const activityIcons = {
|
|
Key,
|
|
ShieldAlert,
|
|
CheckCircle,
|
|
MailCheck,
|
|
};
|
|
|
|
const handleCopyToken = () => {
|
|
if (token) {
|
|
navigator.clipboard.writeText(token);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<DashboardLayout activeTab={activeTab} setActiveTab={setActiveTab}>
|
|
{/* === BUILDER:TABS (do not remove) === */}
|
|
{/* Hero Welcome Banner */}
|
|
<section className="dashboard-hero animate-fade-in">
|
|
<div className="hero-left">
|
|
<div className="hero-tag">
|
|
<ShieldCheck size={16} />
|
|
<span>Identity & Access Controller</span>
|
|
</div>
|
|
<h1 className="hero-title">
|
|
Welcome back, <span className="gradient-text">{user?.firstName || 'Alex'}</span>
|
|
</h1>
|
|
<p className="hero-description">
|
|
AuthSec base project is operational. Your session is protected with Spring Boot JWT
|
|
authentication and granular role-based security boundaries.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="hero-right">
|
|
<div className="hero-token-pill">
|
|
<Key size={14} />
|
|
<span>JWT: {token ? `${token.slice(0, 14)}...` : 'demo-token'}</span>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
icon={Key}
|
|
onClick={() => setShowTokenModal(true)}
|
|
>
|
|
Inspect Token
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={logout}
|
|
>
|
|
Sign Out
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Metric Stats Cards */}
|
|
<section className="stats-grid">
|
|
{MOCK_STATS.map((stat) => {
|
|
const Icon = statIcons[stat.icon] || Activity;
|
|
return (
|
|
<div key={stat.id} className="stat-card">
|
|
<div className="stat-card-header">
|
|
<span className="stat-card-title">{stat.title}</span>
|
|
<div className="stat-card-icon-box">
|
|
<Icon size={20} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="stat-card-value">{stat.value}</div>
|
|
|
|
<div className="stat-card-footer">
|
|
<span>{stat.subtitle}</span>
|
|
<span className={`stat-trend ${stat.status}`}>{stat.trend}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
{/* Quick Action Buttons */}
|
|
<section>
|
|
<div className="section-header">
|
|
<h2 className="section-title">Quick Actions</h2>
|
|
</div>
|
|
|
|
<div className="quick-actions-grid">
|
|
<div className="quick-action-card" onClick={() => setShowTokenModal(true)}>
|
|
<div className="action-icon-circle">
|
|
<Key size={20} />
|
|
</div>
|
|
<div className="action-details">
|
|
<span className="action-name">Security Token</span>
|
|
<span className="action-desc">View JWT payload & auth headers</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="quick-action-card"
|
|
onClick={() => alert(`User Profile:\nName: ${user?.fullname || user?.firstName}\nEmail: ${user?.email}\nRoles: ${user?.roles?.join(', ')}`)}
|
|
>
|
|
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #06b6d4, #3b82f6)' }}>
|
|
<User size={20} />
|
|
</div>
|
|
<div className="action-details">
|
|
<span className="action-name">User Profile</span>
|
|
<span className="action-desc">Account ID #{user?.userId || '101'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="quick-action-card"
|
|
onClick={() => alert('System Parameters: Spring Boot Service Port 9292\nCORS: Enabled\nToken Lifetime: 24h')}
|
|
>
|
|
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #8b5cf6, #ec4899)' }}>
|
|
<Sliders size={20} />
|
|
</div>
|
|
<div className="action-details">
|
|
<span className="action-name">System Parameters</span>
|
|
<span className="action-desc">Endpoints & security policies</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="quick-action-card"
|
|
onClick={() => setActiveTab('security')}
|
|
>
|
|
<div className="action-icon-circle" style={{ background: 'linear-gradient(135deg, #10b981, #06b6d4)' }}>
|
|
<FileText size={20} />
|
|
</div>
|
|
<div className="action-details">
|
|
<span className="action-name">Audit Reports</span>
|
|
<span className="action-desc">Inspect security logs</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Live Security Activity Stream */}
|
|
<section className="activity-card">
|
|
<div className="section-header">
|
|
<div>
|
|
<h2 className="section-title">Recent Security Events</h2>
|
|
<p style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>
|
|
Audited session entries recorded for this workspace
|
|
</p>
|
|
</div>
|
|
<Badge variant="primary" size="md">
|
|
Live Stream
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="activity-list">
|
|
{MOCK_ACTIVITIES.map((act) => {
|
|
const Icon = activityIcons[act.icon] || Key;
|
|
return (
|
|
<div key={act.id} className="activity-item">
|
|
<div className="activity-left">
|
|
<div className="activity-icon">
|
|
<Icon size={18} />
|
|
</div>
|
|
<div className="activity-info">
|
|
<span className="activity-title">{act.title}</span>
|
|
<span className="activity-desc">
|
|
{act.description} • <strong style={{ color: 'var(--text-secondary)' }}>IP: {act.ip}</strong>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="activity-right">
|
|
<Badge
|
|
variant={
|
|
act.badge === 'SUCCESS' ? 'success' : act.badge === 'SECURITY' ? 'warning' : 'info'
|
|
}
|
|
>
|
|
{act.badge}
|
|
</Badge>
|
|
<span className="activity-time">{act.timestamp}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Token Inspector Modal */}
|
|
{showTokenModal && (
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
background: 'rgba(0, 0, 0, 0.7)',
|
|
backdropFilter: 'blur(6px)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
zIndex: 1000,
|
|
padding: '20px',
|
|
}}
|
|
onClick={() => setShowTokenModal(false)}
|
|
>
|
|
<div
|
|
className="custom-card animate-fade-in"
|
|
style={{ maxWidth: '580px', width: '100%', maxHeight: '90vh', overflowY: 'auto' }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<Key size={20} color="var(--color-primary-light)" />
|
|
<h3 style={{ fontSize: '1.25rem' }}>Session Token Inspector</h3>
|
|
</div>
|
|
<button
|
|
className="btn btn-ghost btn-sm"
|
|
onClick={() => setShowTokenModal(false)}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '16px' }}>
|
|
This Bearer token is automatically attached to API requests in the <code>Authorization</code> header.
|
|
</p>
|
|
|
|
<div className="session-info-card">
|
|
<div className="session-info-row">
|
|
<span>Account Email:</span>
|
|
<strong>{user?.email || 'N/A'}</strong>
|
|
</div>
|
|
<div className="session-info-row">
|
|
<span>User ID:</span>
|
|
<strong>{user?.userId || '101'}</strong>
|
|
</div>
|
|
<div className="session-info-row">
|
|
<span>Assigned Roles:</span>
|
|
<strong>{Array.isArray(user?.roles) ? user.roles.join(', ') : 'ROLE_ADMIN'}</strong>
|
|
</div>
|
|
<div className="session-info-row">
|
|
<span>Mode:</span>
|
|
<strong>{isDemoMode ? 'Sandbox Demo Mode' : 'Live Spring Boot'}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ marginTop: '16px' }}>
|
|
<label style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
|
|
Raw JWT Token:
|
|
</label>
|
|
<textarea
|
|
readOnly
|
|
value={token || ''}
|
|
style={{
|
|
width: '100%',
|
|
height: '80px',
|
|
background: 'var(--bg-input)',
|
|
color: 'var(--text-main)',
|
|
border: '1px solid var(--border-subtle)',
|
|
borderRadius: 'var(--radius-sm)',
|
|
padding: '10px',
|
|
fontSize: '0.75rem',
|
|
fontFamily: 'monospace',
|
|
resize: 'none',
|
|
marginTop: '6px',
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', marginTop: '20px' }}>
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
icon={Copy}
|
|
onClick={handleCopyToken}
|
|
>
|
|
{copied ? 'Copied to Clipboard!' : 'Copy Token'}
|
|
</Button>
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
onClick={() => setShowTokenModal(false)}
|
|
>
|
|
Done
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DashboardLayout>
|
|
);
|
|
}
|