|
@@ -1,7 +1,9 @@
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
-import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, Info, X, Shield, Printer, Cylinder, Wifi, Home, Video } from 'lucide-react';
|
|
|
|
|
|
|
+import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, Info, X, Shield, Printer, Cylinder, Wifi, Home, Video, Users, Lock, Unlock } from 'lucide-react';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
+import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
import { api } from '../api/client';
|
|
import { api } from '../api/client';
|
|
|
|
|
+import { useAuth } from '../contexts/AuthContext';
|
|
|
import { formatDateOnly } from '../utils/date';
|
|
import { formatDateOnly } from '../utils/date';
|
|
|
import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus } from '../api/client';
|
|
import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus } from '../api/client';
|
|
|
import { Card, CardContent, CardHeader } from '../components/Card';
|
|
import { Card, CardContent, CardHeader } from '../components/Card';
|
|
@@ -27,10 +29,16 @@ import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, t
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
import { Palette } from 'lucide-react';
|
|
import { Palette } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
+const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', 'apikeys', 'virtual-printer', 'users'] as const;
|
|
|
|
|
+type TabType = typeof validTabs[number];
|
|
|
|
|
+
|
|
|
export function SettingsPage() {
|
|
export function SettingsPage() {
|
|
|
const queryClient = useQueryClient();
|
|
const queryClient = useQueryClient();
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
|
|
+ const [searchParams, setSearchParams] = useSearchParams();
|
|
|
const { t, i18n } = useTranslation();
|
|
const { t, i18n } = useTranslation();
|
|
|
const { showToast, showPersistentToast, dismissToast } = useToast();
|
|
const { showToast, showPersistentToast, dismissToast } = useToast();
|
|
|
|
|
+ const { authEnabled, user, refreshAuth } = useAuth();
|
|
|
const {
|
|
const {
|
|
|
mode,
|
|
mode,
|
|
|
darkStyle, darkBackground, darkAccent,
|
|
darkStyle, darkBackground, darkAccent,
|
|
@@ -46,7 +54,22 @@ export function SettingsPage() {
|
|
|
const [editingTemplate, setEditingTemplate] = useState<NotificationTemplate | null>(null);
|
|
const [editingTemplate, setEditingTemplate] = useState<NotificationTemplate | null>(null);
|
|
|
const [showLogViewer, setShowLogViewer] = useState(false);
|
|
const [showLogViewer, setShowLogViewer] = useState(false);
|
|
|
const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
|
|
const [defaultView, setDefaultViewState] = useState<string>(getDefaultView());
|
|
|
- const [activeTab, setActiveTab] = useState<'general' | 'network' | 'plugs' | 'notifications' | 'filament' | 'apikeys' | 'virtual-printer'>('general');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize tab from URL params
|
|
|
|
|
+ const tabParam = searchParams.get('tab');
|
|
|
|
|
+ const initialTab = tabParam && validTabs.includes(tabParam as TabType) ? tabParam as TabType : 'general';
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState<TabType>(initialTab);
|
|
|
|
|
+
|
|
|
|
|
+ // Update URL when tab changes
|
|
|
|
|
+ const handleTabChange = (tab: TabType) => {
|
|
|
|
|
+ setActiveTab(tab);
|
|
|
|
|
+ if (tab === 'general') {
|
|
|
|
|
+ searchParams.delete('tab');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ searchParams.set('tab', tab);
|
|
|
|
|
+ }
|
|
|
|
|
+ setSearchParams(searchParams, { replace: true });
|
|
|
|
|
+ };
|
|
|
const [showCreateAPIKey, setShowCreateAPIKey] = useState(false);
|
|
const [showCreateAPIKey, setShowCreateAPIKey] = useState(false);
|
|
|
const [newAPIKeyName, setNewAPIKeyName] = useState('');
|
|
const [newAPIKeyName, setNewAPIKeyName] = useState('');
|
|
|
const [newAPIKeyPermissions, setNewAPIKeyPermissions] = useState({
|
|
const [newAPIKeyPermissions, setNewAPIKeyPermissions] = useState({
|
|
@@ -66,6 +89,7 @@ export function SettingsPage() {
|
|
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
|
const [showTelemetryInfo, setShowTelemetryInfo] = useState(false);
|
|
const [showTelemetryInfo, setShowTelemetryInfo] = useState(false);
|
|
|
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
|
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
|
|
|
|
+ const [showDisableAuthConfirm, setShowDisableAuthConfirm] = useState(false);
|
|
|
|
|
|
|
|
// Home Assistant test connection state
|
|
// Home Assistant test connection state
|
|
|
const [haTestResult, setHaTestResult] = useState<{ success: boolean; message: string | null; error: string | null } | null>(null);
|
|
const [haTestResult, setHaTestResult] = useState<{ success: boolean; message: string | null; error: string | null } | null>(null);
|
|
@@ -310,7 +334,12 @@ export function SettingsPage() {
|
|
|
// Sync local state when settings load
|
|
// Sync local state when settings load
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (settings && !localSettings) {
|
|
if (settings && !localSettings) {
|
|
|
- setLocalSettings(settings);
|
|
|
|
|
|
|
+ // Auto-detect external_url from browser if not set
|
|
|
|
|
+ const settingsWithExternalUrl = {
|
|
|
|
|
+ ...settings,
|
|
|
|
|
+ external_url: settings.external_url || window.location.origin,
|
|
|
|
|
+ };
|
|
|
|
|
+ setLocalSettings(settingsWithExternalUrl);
|
|
|
// Mark initial load complete after a short delay
|
|
// Mark initial load complete after a short delay
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
isInitialLoadRef.current = false;
|
|
isInitialLoadRef.current = false;
|
|
@@ -376,6 +405,7 @@ export function SettingsPage() {
|
|
|
settings.mqtt_password !== localSettings.mqtt_password ||
|
|
settings.mqtt_password !== localSettings.mqtt_password ||
|
|
|
settings.mqtt_topic_prefix !== localSettings.mqtt_topic_prefix ||
|
|
settings.mqtt_topic_prefix !== localSettings.mqtt_topic_prefix ||
|
|
|
settings.mqtt_use_tls !== localSettings.mqtt_use_tls ||
|
|
settings.mqtt_use_tls !== localSettings.mqtt_use_tls ||
|
|
|
|
|
+ settings.external_url !== localSettings.external_url ||
|
|
|
settings.ha_enabled !== localSettings.ha_enabled ||
|
|
settings.ha_enabled !== localSettings.ha_enabled ||
|
|
|
settings.ha_url !== localSettings.ha_url ||
|
|
settings.ha_url !== localSettings.ha_url ||
|
|
|
settings.ha_token !== localSettings.ha_token ||
|
|
settings.ha_token !== localSettings.ha_token ||
|
|
@@ -436,6 +466,7 @@ export function SettingsPage() {
|
|
|
mqtt_password: localSettings.mqtt_password,
|
|
mqtt_password: localSettings.mqtt_password,
|
|
|
mqtt_topic_prefix: localSettings.mqtt_topic_prefix,
|
|
mqtt_topic_prefix: localSettings.mqtt_topic_prefix,
|
|
|
mqtt_use_tls: localSettings.mqtt_use_tls,
|
|
mqtt_use_tls: localSettings.mqtt_use_tls,
|
|
|
|
|
+ external_url: localSettings.external_url,
|
|
|
ha_enabled: localSettings.ha_enabled,
|
|
ha_enabled: localSettings.ha_enabled,
|
|
|
ha_url: localSettings.ha_url,
|
|
ha_url: localSettings.ha_url,
|
|
|
ha_token: localSettings.ha_token,
|
|
ha_token: localSettings.ha_token,
|
|
@@ -474,9 +505,9 @@ export function SettingsPage() {
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Tab Navigation */}
|
|
{/* Tab Navigation */}
|
|
|
- <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary">
|
|
|
|
|
|
|
+ <div className="flex gap-1 mb-6 border-b border-bambu-dark-tertiary overflow-x-auto">
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('general')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('general')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
|
activeTab === 'general'
|
|
activeTab === 'general'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -486,7 +517,7 @@ export function SettingsPage() {
|
|
|
General
|
|
General
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('plugs')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('plugs')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'plugs'
|
|
activeTab === 'plugs'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -502,7 +533,7 @@ export function SettingsPage() {
|
|
|
)}
|
|
)}
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('notifications')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('notifications')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'notifications'
|
|
activeTab === 'notifications'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -518,7 +549,7 @@ export function SettingsPage() {
|
|
|
)}
|
|
)}
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('filament')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('filament')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'filament'
|
|
activeTab === 'filament'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -529,7 +560,7 @@ export function SettingsPage() {
|
|
|
Filament
|
|
Filament
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('network')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('network')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'network'
|
|
activeTab === 'network'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -541,7 +572,7 @@ export function SettingsPage() {
|
|
|
<span className={`w-2 h-2 rounded-full ${mqttStatus?.enabled ? 'bg-green-400' : 'bg-gray-500'}`} />
|
|
<span className={`w-2 h-2 rounded-full ${mqttStatus?.enabled ? 'bg-green-400' : 'bg-gray-500'}`} />
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('apikeys')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('apikeys')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'apikeys'
|
|
activeTab === 'apikeys'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -557,7 +588,7 @@ export function SettingsPage() {
|
|
|
)}
|
|
)}
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
- onClick={() => setActiveTab('virtual-printer')}
|
|
|
|
|
|
|
+ onClick={() => handleTabChange('virtual-printer')}
|
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
activeTab === 'virtual-printer'
|
|
activeTab === 'virtual-printer'
|
|
|
? 'text-bambu-green border-bambu-green'
|
|
? 'text-bambu-green border-bambu-green'
|
|
@@ -568,6 +599,20 @@ export function SettingsPage() {
|
|
|
Virtual Printer
|
|
Virtual Printer
|
|
|
<span className={`w-2 h-2 rounded-full ${virtualPrinterRunning ? 'bg-green-400' : 'bg-gray-500'}`} />
|
|
<span className={`w-2 h-2 rounded-full ${virtualPrinterRunning ? 'bg-green-400' : 'bg-gray-500'}`} />
|
|
|
</button>
|
|
</button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => handleTabChange('users')}
|
|
|
|
|
+ className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px flex items-center gap-2 ${
|
|
|
|
|
+ activeTab === 'users'
|
|
|
|
|
+ ? 'text-bambu-green border-bambu-green'
|
|
|
|
|
+ : 'text-bambu-gray hover:text-gray-900 dark:hover:text-white border-transparent'
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Users className="w-4 h-4" />
|
|
|
|
|
+ Users
|
|
|
|
|
+ {authEnabled && (
|
|
|
|
|
+ <span className={`w-2 h-2 rounded-full ${authEnabled ? 'bg-green-400' : 'bg-gray-500'}`} />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* General Tab */}
|
|
{/* General Tab */}
|
|
@@ -1286,8 +1331,38 @@ export function SettingsPage() {
|
|
|
{/* Network Tab */}
|
|
{/* Network Tab */}
|
|
|
{activeTab === 'network' && localSettings && (
|
|
{activeTab === 'network' && localSettings && (
|
|
|
<div className="flex flex-col lg:flex-row gap-6">
|
|
<div className="flex flex-col lg:flex-row gap-6">
|
|
|
- {/* Left Column - FTP Retry & Home Assistant */}
|
|
|
|
|
|
|
+ {/* Left Column - External URL & FTP Retry */}
|
|
|
<div className="flex-1 lg:max-w-xl space-y-4">
|
|
<div className="flex-1 lg:max-w-xl space-y-4">
|
|
|
|
|
+ {/* External URL */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
|
|
|
+ <Globe className="w-5 h-5 text-blue-400" />
|
|
|
|
|
+ External URL
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent className="space-y-4">
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">
|
|
|
|
|
+ The external URL where Bambuddy is accessible. Used for notification images and external integrations.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <label className="block text-sm text-bambu-gray mb-1">
|
|
|
|
|
+ Bambuddy URL
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ value={localSettings.external_url ?? ''}
|
|
|
|
|
+ onChange={(e) => updateSetting('external_url', e.target.value)}
|
|
|
|
|
+ placeholder="http://192.168.1.100:8000"
|
|
|
|
|
+ className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
|
|
|
|
+ />
|
|
|
|
|
+ <p className="text-xs text-bambu-gray mt-1">
|
|
|
|
|
+ Include protocol and port (e.g., http://192.168.1.100:8000)
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
@@ -1384,6 +1459,10 @@ export function SettingsPage() {
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Right Column - Home Assistant & MQTT Publishing */}
|
|
|
|
|
+ <div className="flex-1 lg:max-w-xl space-y-4">
|
|
|
{/* Home Assistant Integration */}
|
|
{/* Home Assistant Integration */}
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
@@ -1482,10 +1561,8 @@ export function SettingsPage() {
|
|
|
)}
|
|
)}
|
|
|
</CardContent>
|
|
</CardContent>
|
|
|
</Card>
|
|
</Card>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- {/* Right Column - MQTT Publishing */}
|
|
|
|
|
- <div className="flex-1 lg:max-w-xl space-y-4">
|
|
|
|
|
|
|
+ {/* MQTT Publishing */}
|
|
|
<Card>
|
|
<Card>
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center justify-between">
|
|
@@ -2866,6 +2943,230 @@ export function SettingsPage() {
|
|
|
</Card>
|
|
</Card>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Users Tab */}
|
|
|
|
|
+ {activeTab === 'users' && (
|
|
|
|
|
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
|
|
|
+ <Users className="w-5 h-5 text-bambu-green" />
|
|
|
|
|
+ User Authentication
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray mt-1">
|
|
|
|
|
+ Enable authentication to secure your Bambuddy instance and manage user access.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardContent className="py-6">
|
|
|
|
|
+ {!authEnabled ? (
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <div className={`w-12 h-12 rounded-full flex items-center justify-center ${authEnabled ? 'bg-green-500/20' : 'bg-gray-500/20'}`}>
|
|
|
|
|
+ {authEnabled ? (
|
|
|
|
|
+ <Lock className="w-6 h-6 text-green-400" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Unlock className="w-6 h-6 text-gray-400" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <h3 className="text-white font-medium">Authentication Disabled</h3>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">
|
|
|
|
|
+ Your Bambuddy instance is currently accessible without authentication.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="pt-4 border-t border-bambu-dark-tertiary">
|
|
|
|
|
+ <p className="text-sm text-bambu-gray mb-4">
|
|
|
|
|
+ Enable authentication to:
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <ul className="space-y-2 text-sm text-bambu-gray mb-4">
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Require login to access the system</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Manage multiple users with different roles</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Control access to printer settings and user management</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={(e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ navigate('/setup');
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Lock className="w-4 h-4" />
|
|
|
|
|
+ Activate Authentication
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <div className="w-12 h-12 rounded-full flex items-center justify-center bg-green-500/20">
|
|
|
|
|
+ <Lock className="w-6 h-6 text-green-400" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <h3 className="text-white font-medium">Authentication Enabled</h3>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">
|
|
|
|
|
+ Your Bambuddy instance is secured with authentication.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {user && (
|
|
|
|
|
+ <div className="pt-4 border-t border-bambu-dark-tertiary">
|
|
|
|
|
+ <div className="flex items-center justify-between mb-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray">Current User</p>
|
|
|
|
|
+ <p className="text-white font-medium">{user.username}</p>
|
|
|
|
|
+ <p className="text-xs text-bambu-gray mt-1">
|
|
|
|
|
+ Role: <span className="capitalize">{user.role}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className={`px-3 py-1 rounded-full text-xs font-medium ${
|
|
|
|
|
+ user.role === 'admin'
|
|
|
|
|
+ ? 'bg-purple-500/20 text-purple-300'
|
|
|
|
|
+ : 'bg-blue-500/20 text-blue-300'
|
|
|
|
|
+ }`}>
|
|
|
|
|
+ {user.role === 'admin' ? 'Admin' : 'User'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ <div className="pt-4 border-t border-bambu-dark-tertiary space-y-3">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => navigate('/users')}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ variant="secondary"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Users className="w-4 h-4" />
|
|
|
|
|
+ Manage Users
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
|
|
+ {user?.role === 'admin' && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={() => setShowDisableAuthConfirm(true)}
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ variant="secondary"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Unlock className="w-4 h-4" />
|
|
|
|
|
+ Disable Authentication
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {authEnabled && (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="mb-6">
|
|
|
|
|
+ <h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
|
|
|
+ <Shield className="w-5 h-5 text-bambu-green" />
|
|
|
|
|
+ Role Permissions
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <p className="text-sm text-bambu-gray mt-1">
|
|
|
|
|
+ Overview of what each role can do.
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <div className="w-8 h-8 rounded-full bg-purple-500/20 flex items-center justify-center">
|
|
|
|
|
+ <Shield className="w-4 h-4 text-purple-300" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <h3 className="text-white font-medium">Admin</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <ul className="space-y-2 text-sm text-bambu-gray">
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Manage printer settings</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Create, edit, and delete users</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Access all system features</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <div className="w-8 h-8 rounded-full bg-blue-500/20 flex items-center justify-center">
|
|
|
|
|
+ <Users className="w-4 h-4 text-blue-300" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <h3 className="text-white font-medium">User</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <ul className="space-y-2 text-sm text-bambu-gray">
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Send print jobs</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Manage files and archives</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ <li className="flex items-start gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-bambu-green mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <span>Manage filament</span>
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Disable Authentication Confirmation Modal */}
|
|
|
|
|
+ {showDisableAuthConfirm && (
|
|
|
|
|
+ <ConfirmModal
|
|
|
|
|
+ title="Disable Authentication"
|
|
|
|
|
+ message="Are you sure you want to disable authentication? This will make your Bambuddy instance accessible without login. All users will remain in the database but authentication will be disabled."
|
|
|
|
|
+ confirmText="Disable Authentication"
|
|
|
|
|
+ variant="danger"
|
|
|
|
|
+ onConfirm={async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await api.disableAuth();
|
|
|
|
|
+ showToast('Authentication disabled successfully', 'success');
|
|
|
|
|
+ await refreshAuth();
|
|
|
|
|
+ setShowDisableAuthConfirm(false);
|
|
|
|
|
+ // Refresh the page to ensure all protected routes are accessible
|
|
|
|
|
+ window.location.href = '/';
|
|
|
|
|
+ } catch (error: unknown) {
|
|
|
|
|
+ const message = error instanceof Error ? error.message : 'Failed to disable authentication';
|
|
|
|
|
+ showToast(message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ onCancel={() => setShowDisableAuthConfirm(false)}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|