Add dual user classes: admin + tech users with PIN login

- Add tech user management (JSON-backed CRUD with PIN auth)
- Dual login: tabbed Tech Login (username+PIN) / Admin Login (NC credentials)
- Admin panel: tappable user list with detail modal (enable/disable, reset PIN, reset NC password, delete)
- Auto-provision Nextcloud accounts for tech users
- Admin guard: tech users redirected away from admin panel
- New data volume for persistent tech_users.json storage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 00:17:26 -06:00
parent ca03f6e143
commit 99fb5ff7e7
13 changed files with 1143 additions and 493 deletions

View File

@@ -1,5 +1,6 @@
from flask import Blueprint, request, jsonify, session
from app.services.nextcloud import NextcloudClient
from app.services import tech_users
from config import Config
import base64
@@ -15,42 +16,74 @@ def _decrypt_password(encrypted: str) -> str:
@bp.route('/login', methods=['POST'])
def login():
"""Authenticate user against Nextcloud."""
"""Authenticate admin user against Nextcloud."""
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
return jsonify({'error': 'Username and password required'}), 400
username = data['username'].strip()
password = data['password']
if not username or not password:
return jsonify({'error': 'Username and password cannot be empty'}), 400
try:
# Validate credentials by attempting to connect to Nextcloud
nc_client = NextcloudClient(Config.NEXTCLOUD_URL, username, password)
if not nc_client.verify_credentials():
return jsonify({'error': 'Invalid username or password'}), 401
# Check if user is admin
is_admin = nc_client.check_admin()
# Store credentials in session
if not is_admin:
return jsonify({'error': 'Admin credentials required. Tech users must use the Tech login.'}), 403
session['username'] = username
session['password'] = _encrypt_password(password)
session['is_admin'] = is_admin
session['is_admin'] = True
session['user_type'] = 'admin'
return jsonify({
'success': True,
'username': username,
'is_admin': is_admin
'is_admin': True,
'user_type': 'admin',
}), 200
except Exception as e:
return jsonify({'error': f'Authentication failed: {str(e)}'}), 500
@bp.route('/login/tech', methods=['POST'])
def login_tech():
"""Authenticate tech user with username + PIN."""
data = request.get_json()
if not data or 'username' not in data or 'pin' not in data:
return jsonify({'error': 'Username and PIN required'}), 400
username = data['username'].strip()
pin = data['pin']
if not username or not pin:
return jsonify({'error': 'Username and PIN cannot be empty'}), 400
user = tech_users.verify_pin(username, pin)
if not user:
return jsonify({'error': 'Invalid username or PIN'}), 401
# Store the generated NC password so API calls work transparently
session['username'] = username
session['password'] = _encrypt_password(user['nc_password'])
session['is_admin'] = False
session['user_type'] = 'tech'
return jsonify({
'success': True,
'username': username,
'is_admin': False,
'user_type': 'tech',
}), 200
@bp.route('/logout', methods=['POST'])
def logout():
"""Clear user session."""
@@ -64,7 +97,8 @@ def status():
return jsonify({
'authenticated': True,
'username': session['username'],
'is_admin': session.get('is_admin', False)
'is_admin': session.get('is_admin', False),
'user_type': session.get('user_type', 'admin'),
}), 200
else:
return jsonify({