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:
2026-04-26 05:51:23 +00:00
parent 888acdbd7e
commit b1697d6e1e
17 changed files with 216 additions and 221 deletions

View File

@@ -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}`}
>