nas-samba v2.0.0: per-user home directories, Samba accounts, remove quotas
- Each user gets a Linux + Samba account and home dir at /nas/home/<username>/ - Web UI file browsing scoped to per-user directories - Creating/deleting users from web UI also manages system + Samba accounts - Replaced hardcoded [data] SMB share with [homes] (per-user automatic shares) - Removed hardcoded alexz user from entrypoint — server.js handles all user management - Removed storage quotas from backend and frontend - Existing users get home directories auto-created on startup (migration)
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
MoreVertical, Download, Trash2, Edit3, Move, Copy,
|
||||
Share2, ChevronRight, Home, Moon, Sun, LogOut, Users,
|
||||
Activity, X, Upload, Folder, File, Image,
|
||||
Film, Music, Archive, Code, FileText, HardDrive,
|
||||
Film, Music, Archive, Code, FileText,
|
||||
Link, Shield
|
||||
} from 'lucide-react';
|
||||
import { formatBytes, formatDate, cn } from '../lib/utils';
|
||||
@@ -159,42 +159,9 @@ function Modal({ open, onClose, title, children, size = 'md' }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Storage Usage Component
|
||||
function StorageUsage({ used, quota }) {
|
||||
const percentage = quota > 0 ? (used / quota) * 100 : 0;
|
||||
const isWarning = percentage > 80;
|
||||
const isCritical = percentage > 95;
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-card border border-border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Storage</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatBytes(used)} / {formatBytes(quota)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full transition-all duration-300",
|
||||
isCritical ? "bg-destructive" : isWarning ? "bg-amber-500" : "bg-primary"
|
||||
)}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{percentage.toFixed(1)}% used
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Main Dashboard Component
|
||||
function Dashboard() {
|
||||
const { user, logout, updateUser } = useAuth();
|
||||
const { user, logout } = useAuth();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
// State
|
||||
@@ -232,7 +199,7 @@ function Dashboard() {
|
||||
const [loadingUsers, setLoadingUsers] = useState(false);
|
||||
const [showUserForm, setShowUserForm] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState(null);
|
||||
const [userForm, setUserForm] = useState({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 });
|
||||
const [userForm, setUserForm] = useState({ username: '', password: '', name: '', role: 'user' });
|
||||
|
||||
// Activity state
|
||||
const [activities, setActivities] = useState([]);
|
||||
@@ -274,11 +241,10 @@ function Dashboard() {
|
||||
try {
|
||||
const response = await api.getStats();
|
||||
setStats(response.data);
|
||||
updateUser({ storage_used: response.data.storage_used });
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats');
|
||||
}
|
||||
}, [updateUser]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
@@ -467,7 +433,7 @@ function Dashboard() {
|
||||
}
|
||||
try {
|
||||
if (editingUser) {
|
||||
const updateData = { name: userForm.name, role: userForm.role, storage_quota: userForm.storage_quota };
|
||||
const updateData = { name: userForm.name, role: userForm.role };
|
||||
if (userForm.password) updateData.password = userForm.password;
|
||||
await api.updateUser(editingUser.id, updateData);
|
||||
toast.success('User updated');
|
||||
@@ -477,7 +443,7 @@ function Dashboard() {
|
||||
}
|
||||
setShowUserForm(false);
|
||||
setEditingUser(null);
|
||||
setUserForm({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 });
|
||||
setUserForm({ username: '', password: '', name: '', role: 'user' });
|
||||
loadUsers();
|
||||
} catch (error) {
|
||||
toast.error(error.response?.data?.detail || 'Failed to save user');
|
||||
@@ -661,12 +627,7 @@ function Dashboard() {
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
||||
{/* Storage & Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<StorageUsage
|
||||
used={stats?.storage_used || user?.storage_used || 0}
|
||||
quota={stats?.storage_quota || user?.storage_quota || 10737418240}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-card border border-border rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<File className="w-4 h-4 text-muted-foreground" />
|
||||
@@ -1212,24 +1173,9 @@ function Dashboard() {
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Storage Quota</label>
|
||||
<select
|
||||
value={userForm.storage_quota}
|
||||
onChange={(e) => setUserForm(prev => ({ ...prev, storage_quota: Number(e.target.value) }))}
|
||||
data-testid="user-quota-select"
|
||||
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value={1073741824}>1 GB</option>
|
||||
<option value={5368709120}>5 GB</option>
|
||||
<option value={10737418240}>10 GB</option>
|
||||
<option value={53687091200}>50 GB</option>
|
||||
<option value={107374182400}>100 GB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={() => { setShowUserForm(false); setEditingUser(null); setUserForm({ username: '', password: '', name: '', role: 'user', storage_quota: 10737418240 }); }}
|
||||
onClick={() => { setShowUserForm(false); setEditingUser(null); setUserForm({ username: '', password: '', name: '', role: 'user' }); }}
|
||||
className="flex-1 h-10 bg-secondary text-secondary-foreground rounded-lg hover:opacity-80"
|
||||
>
|
||||
Cancel
|
||||
@@ -1279,10 +1225,10 @@ function Dashboard() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatBytes(u.storage_used)} / {formatBytes(u.storage_quota)}
|
||||
{u.role}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { setEditingUser(u); setUserForm({ username: u.username, password: '', name: u.name, role: u.role, storage_quota: u.storage_quota }); setShowUserForm(true); }}
|
||||
onClick={() => { setEditingUser(u); setUserForm({ username: u.username, password: '', name: u.name, role: u.role }); setShowUserForm(true); }}
|
||||
className="p-2 hover:bg-accent rounded-lg"
|
||||
data-testid={`edit-user-${u.id}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user