Phase 2.5: UI redesign, inline quotas, toasts, Users tab
- Full UI redesign: GitHub-dark palette, custom CSS vars, better contrast, card layout, cleaner tables and typography - Dashboard auto-refresh every 30s (was 60s) - Remove Quotas tab; each share row now has a Quota button that opens a modal pre-filled with the share's valid users (SMB) or path (NFS) - Replace all alert() with Bootstrap toast notifications - Add Users tab: list system users (uid >= 1000) with SMB status, create user (useradd + smbpasswd), delete user, change SMB password - Backend: /api/users GET/POST, /api/users/<name> DELETE, /api/users/<name>/password POST Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d237f4d290
commit
822e6b3b87
3 files changed
+561
-180
No files matched your search
@@ -220,6 +220,101 @@ def set_quota():
|
||||
return jsonify({'status': 'ok'})
|
||||
|
||||
|
||||
# ===== USERS =====
|
||||
|
||||
def _smb_users():
|
||||
try:
|
||||
r = subprocess.run(['pdbedit', '-L'], capture_output=True, text=True)
|
||||
return {line.split(':')[0].strip() for line in r.stdout.splitlines() if ':' in line}
|
||||
except FileNotFoundError:
|
||||
return set()
|
||||
|
||||
|
||||
def _list_users():
|
||||
smb = _smb_users()
|
||||
users = []
|
||||
with open('/etc/passwd', 'r') as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(':')
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
name, _, uid = parts[0], parts[1], parts[2]
|
||||
uid = int(uid)
|
||||
if uid < 1000 or uid == 65534:
|
||||
continue
|
||||
users.append({'name': name, 'uid': uid, 'smb': name in smb})
|
||||
return users
|
||||
|
||||
|
||||
def _set_smb_password(name, password):
|
||||
r = subprocess.run(
|
||||
['smbpasswd', '-a', '-s', name],
|
||||
input=f'{password}\n{password}\n',
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(r.stderr.strip() or 'smbpasswd failed')
|
||||
|
||||
|
||||
@app.route('/api/users', methods=['GET'])
|
||||
@require_auth
|
||||
def list_users():
|
||||
return jsonify(_list_users())
|
||||
|
||||
|
||||
@app.route('/api/users', methods=['POST'])
|
||||
@require_auth
|
||||
def create_user():
|
||||
data = request.json or {}
|
||||
name = data.get('name', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
if not VALID_NAME.match(name):
|
||||
return jsonify({'error': 'invalid username (letters, digits, _ -)'}), 400
|
||||
if len(password) < 8:
|
||||
return jsonify({'error': 'password must be at least 8 characters'}), 400
|
||||
|
||||
try:
|
||||
subprocess.run(['useradd', '-M', '-s', '/usr/sbin/nologin', name], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
return jsonify({'error': f'user {name!r} already exists or useradd failed'}), 400
|
||||
try:
|
||||
_set_smb_password(name, password)
|
||||
except RuntimeError as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
return jsonify({'status': 'created'})
|
||||
|
||||
|
||||
@app.route('/api/users/<name>', methods=['DELETE'])
|
||||
@require_auth
|
||||
def delete_user(name):
|
||||
if not VALID_NAME.match(name):
|
||||
return jsonify({'error': 'invalid username'}), 400
|
||||
subprocess.run(['smbpasswd', '-x', name], capture_output=True)
|
||||
try:
|
||||
subprocess.run(['userdel', name], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
return jsonify({'error': f'userdel failed: {e}'}), 500
|
||||
return jsonify({'status': 'deleted'})
|
||||
|
||||
|
||||
@app.route('/api/users/<name>/password', methods=['POST'])
|
||||
@require_auth
|
||||
def change_password(name):
|
||||
if not VALID_NAME.match(name):
|
||||
return jsonify({'error': 'invalid username'}), 400
|
||||
data = request.json or {}
|
||||
password = data.get('password', '')
|
||||
if len(password) < 8:
|
||||
return jsonify({'error': 'password must be at least 8 characters'}), 400
|
||||
try:
|
||||
_set_smb_password(name, password)
|
||||
except RuntimeError as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
return jsonify({'status': 'updated'})
|
||||
|
||||
|
||||
# ===== DASHBOARD =====
|
||||
|
||||
def _daemon_status():
|
||||
|
||||
Reference in new issue
Block a user