Files
nextsnap/app/routes/auth.py
kamaji 99fb5ff7e7 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>
2026-02-08 00:17:26 -06:00

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