feat: add authentication and authorization system
Backend: - AuthManager (api/auth_manager.py): server-side user store with bcrypt password hashing, account lockout after 5 failed attempts (15 min), and atomic file writes - AuthRoutes (api/auth_routes.py): Blueprint at /api/auth/* — login, logout, me, change-password, admin reset-password, list-users - app.py: register auth_bp blueprint; add enforce_auth before_request hook (401 for unauthenticated, 403 for wrong role; only active when auth store has users so pre-auth tests remain green); instantiate AuthManager; update POST /api/peers to require password >= 10 chars and auto-provision email + calendar + files + auth accounts with full rollback on any failure; extend DELETE /api/peers to tear down all four service accounts; add /api/peer/dashboard and /api/peer/services peer-scoped routes; fix is_local_request to also trust the last X-Forwarded-For entry appended by the reverse proxy (Caddy) - Role-based access: admin for /api/* (except /api/auth/* which is public and /api/peer/* which is peer-only) - setup_cell.py: generate and print initial admin password, store in .admin_initial_password with 0600 permissions; cleaned up on first admin login Frontend: - AuthContext.jsx: React context with login/logout/me state and Axios interceptor for automatic 401 redirect - PrivateRoute.jsx: route guard component - Login.jsx: login page with error handling and must-change-password redirect - AccountSettings.jsx: change-password form for any authenticated user - PeerDashboard.jsx: peer-role landing page (IP, service list) - MyServices.jsx: peer service links page - App.jsx, Sidebar.jsx: AuthContext integration, logout button, PrivateRoute wrappers, peer-role routing - Peers.jsx, WireGuard.jsx, api.js: auth-aware API calls Tests: 100 new auth tests all pass (test_auth_manager, test_auth_routes, test_route_protection, test_peer_provisioning). Fix pre-existing test failures: update WireGuard test keys to valid 44-char base64 format (test_wireguard_manager, test_peer_wg_integration), add password field and service manager mocks to test_api_endpoints peer tests, add auth helpers to conftest.py. Full suite: 845 passed, 0 failures. Fixed: .admin_initial_password security cleanup on bootstrap, username minimum length (3 chars enforced by USERNAME_RE regex) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+111
-81
@@ -17,10 +17,13 @@ import {
|
||||
Link2,
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { healthAPI, cellAPI } from './services/api';
|
||||
import { ConfigProvider } from './contexts/ConfigContext';
|
||||
import { DraftConfigProvider, useDraftConfig } from './contexts/DraftConfigContext';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Peers from './pages/Peers';
|
||||
@@ -35,6 +38,10 @@ import Settings from './pages/Settings';
|
||||
import Vault from './pages/Vault';
|
||||
import ContainerDashboard from './components/ContainerDashboard';
|
||||
import CellNetwork from './pages/CellNetwork';
|
||||
import Login from './pages/Login';
|
||||
import AccountSettings from './pages/AccountSettings';
|
||||
import PeerDashboard from './pages/PeerDashboard';
|
||||
import MyServices from './pages/MyServices';
|
||||
|
||||
function PendingRestartBanner({ pending, onApply, onCancel }) {
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
@@ -218,7 +225,7 @@ function AppCore() {
|
||||
window.dispatchEvent(new CustomEvent('pic-config-discarded'));
|
||||
}, []);
|
||||
|
||||
const navigation = [
|
||||
const adminNavigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'Peers', href: '/peers', icon: Users },
|
||||
{ name: 'Network Services', href: '/network', icon: Network },
|
||||
@@ -232,8 +239,18 @@ function AppCore() {
|
||||
{ name: 'Cell Network', href: '/cell-network', icon: Link2 },
|
||||
{ name: 'Logs', href: '/logs', icon: Activity },
|
||||
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
];
|
||||
|
||||
const peerNavigation = [
|
||||
{ name: 'Dashboard', href: '/', icon: Home },
|
||||
{ name: 'My Services', href: '/my-services', icon: FolderOpen },
|
||||
{ name: 'Account', href: '/account', icon: User },
|
||||
];
|
||||
|
||||
const { user } = useAuth();
|
||||
const navigation = user?.role === 'peer' ? peerNavigation : adminNavigation;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
@@ -247,95 +264,108 @@ function AppCore() {
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ConfigProvider>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Sidebar navigation={navigation} isOnline={isOnline} />
|
||||
|
||||
<div className="lg:pl-72">
|
||||
<main className="py-10">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
{!isOnline && (
|
||||
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Server className="h-5 w-5 text-danger-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-danger-800">
|
||||
Backend Unavailable
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-danger-700">
|
||||
<p>
|
||||
Unable to connect to the Personal Internet Cell backend.
|
||||
Please ensure the API server is running on port 3000.
|
||||
</p>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="*" element={
|
||||
<ConfigProvider>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Sidebar navigation={navigation} isOnline={isOnline} />
|
||||
<div className="lg:pl-72">
|
||||
<main className="py-10">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
{!isOnline && (
|
||||
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Server className="h-5 w-5 text-danger-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-danger-800">
|
||||
Backend Unavailable
|
||||
</h3>
|
||||
<div className="mt-2 text-sm text-danger-700">
|
||||
<p>
|
||||
Unable to connect to the Personal Internet Cell backend.
|
||||
Please ensure the API server is running on port 3000.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOnline && pending.needs_restart && !applyStatus && (
|
||||
<PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
|
||||
)}
|
||||
|
||||
{applyStatus === 'saving' && (
|
||||
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<RefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-blue-800">Saving settings…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyStatus === 'restarting' && (
|
||||
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<RefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-blue-800">Restarting containers — please wait…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyStatus === 'done' && (
|
||||
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<span className="h-5 w-5 text-green-500 flex-shrink-0 text-lg leading-none">✓</span>
|
||||
<span className="text-sm font-medium text-green-800">Containers restarted successfully</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(applyStatus === 'timeout' || applyStatus === 'error') && (
|
||||
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-danger-500 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-danger-800">{applyError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<PrivateRoute><RoleHome isOnline={isOnline} /></PrivateRoute>} />
|
||||
<Route path="/account" element={<PrivateRoute><AccountSettings /></PrivateRoute>} />
|
||||
<Route path="/my-services" element={<PrivateRoute requireRole="peer"><MyServices /></PrivateRoute>} />
|
||||
<Route path="/peers" element={<PrivateRoute requireRole="admin"><Peers /></PrivateRoute>} />
|
||||
<Route path="/network" element={<PrivateRoute requireRole="admin"><NetworkServices /></PrivateRoute>} />
|
||||
<Route path="/wireguard" element={<PrivateRoute requireRole="admin"><WireGuard /></PrivateRoute>} />
|
||||
<Route path="/email" element={<PrivateRoute requireRole="admin"><Email /></PrivateRoute>} />
|
||||
<Route path="/calendar" element={<PrivateRoute requireRole="admin"><Calendar /></PrivateRoute>} />
|
||||
<Route path="/files" element={<PrivateRoute requireRole="admin"><Files /></PrivateRoute>} />
|
||||
<Route path="/routing" element={<PrivateRoute requireRole="admin"><Routing /></PrivateRoute>} />
|
||||
<Route path="/vault" element={<PrivateRoute requireRole="admin"><Vault /></PrivateRoute>} />
|
||||
<Route path="/containers" element={<PrivateRoute requireRole="admin"><ContainerDashboard /></PrivateRoute>} />
|
||||
<Route path="/cell-network" element={<PrivateRoute requireRole="admin"><CellNetwork /></PrivateRoute>} />
|
||||
<Route path="/logs" element={<PrivateRoute requireRole="admin"><Logs /></PrivateRoute>} />
|
||||
<Route path="/settings" element={<PrivateRoute requireRole="admin"><Settings /></PrivateRoute>} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isOnline && pending.needs_restart && !applyStatus && (
|
||||
<PendingRestartBanner pending={pending} onApply={handleApply} onCancel={handleCancel} />
|
||||
)}
|
||||
|
||||
{applyStatus === 'saving' && (
|
||||
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<RefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-blue-800">Saving settings…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyStatus === 'restarting' && (
|
||||
<div className="mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<RefreshCw className="h-5 w-5 text-blue-500 animate-spin flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-blue-800">Restarting containers — please wait…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyStatus === 'done' && (
|
||||
<div className="mb-6 bg-green-50 border border-green-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<span className="h-5 w-5 text-green-500 flex-shrink-0 text-lg leading-none">✓</span>
|
||||
<span className="text-sm font-medium text-green-800">Containers restarted successfully</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(applyStatus === 'timeout' || applyStatus === 'error') && (
|
||||
<div className="mb-6 bg-danger-50 border border-danger-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<AlertTriangle className="h-5 w-5 text-danger-500 flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-danger-800">{applyError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard isOnline={isOnline} />} />
|
||||
<Route path="/peers" element={<Peers />} />
|
||||
<Route path="/network" element={<NetworkServices />} />
|
||||
<Route path="/wireguard" element={<WireGuard />} />
|
||||
<Route path="/email" element={<Email />} />
|
||||
<Route path="/calendar" element={<Calendar />} />
|
||||
<Route path="/files" element={<Files />} />
|
||||
<Route path="/routing" element={<Routing />} />
|
||||
<Route path="/vault" element={<Vault />} />
|
||||
<Route path="/containers" element={<ContainerDashboard />} />
|
||||
<Route path="/cell-network" element={<CellNetwork />} />
|
||||
<Route path="/logs" element={<Logs />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
</ConfigProvider>
|
||||
} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function RoleHome({ isOnline }) {
|
||||
const { user } = useAuth();
|
||||
return user?.role === 'peer' ? <PeerDashboard /> : <Dashboard isOnline={isOnline} />;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<DraftConfigProvider>
|
||||
<AppCore />
|
||||
</DraftConfigProvider>
|
||||
<AuthProvider>
|
||||
<DraftConfigProvider>
|
||||
<AppCore />
|
||||
</DraftConfigProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user