- 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>
107 lines
3.4 KiB
Python
107 lines
3.4 KiB
Python
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
|
|
|
|
bp = Blueprint('auth', __name__, url_prefix='/api/auth')
|
|
|
|
def _encrypt_password(password: str) -> str:
|
|
"""Simple base64 encoding for password storage in session."""
|
|
return base64.b64encode(password.encode()).decode()
|
|
|
|
def _decrypt_password(encrypted: str) -> str:
|
|
"""Decode base64 encoded password from session."""
|
|
return base64.b64decode(encrypted.encode()).decode()
|
|
|
|
@bp.route('/login', methods=['POST'])
|
|
def login():
|
|
"""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:
|
|
nc_client = NextcloudClient(Config.NEXTCLOUD_URL, username, password)
|
|
|
|
if not nc_client.verify_credentials():
|
|
return jsonify({'error': 'Invalid username or password'}), 401
|
|
|
|
is_admin = nc_client.check_admin()
|
|
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'] = True
|
|
session['user_type'] = 'admin'
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'username': username,
|
|
'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."""
|
|
session.clear()
|
|
return jsonify({'success': True}), 200
|
|
|
|
@bp.route('/status', methods=['GET'])
|
|
def status():
|
|
"""Check current authentication status."""
|
|
if 'username' in session:
|
|
return jsonify({
|
|
'authenticated': True,
|
|
'username': session['username'],
|
|
'is_admin': session.get('is_admin', False),
|
|
'user_type': session.get('user_type', 'admin'),
|
|
}), 200
|
|
else:
|
|
return jsonify({
|
|
'authenticated': False
|
|
}), 200
|