""" Admin Users API Routes - Admin blueprint Migrated from app.py as part of the blueprint refactoring. Contains API routes for AI-powered user parsing and bulk user creation. """ import json import logging import os import re import secrets import string import tempfile from flask import jsonify, request from flask_login import current_user, login_required from werkzeug.security import generate_password_hash from database import SessionLocal, User, Company, SystemRole, CompanyRole, UserCompanyPermissions from utils.decorators import role_required import gemini_service from . import bp logger = logging.getLogger(__name__) # ============================================================ # AI PROMPTS FOR USER PARSING # ============================================================ AI_USER_PARSE_PROMPT = """Jesteś asystentem systemu NordaBiz pomagającym administratorowi tworzyć konta użytkowników. ZADANIE: Przeanalizuj podany tekst i wyodrębnij informacje o użytkownikach. DANE WEJŚCIOWE: ``` {input_text} ``` DOSTĘPNE FIRMY W SYSTEMIE (id: nazwa): {companies_json} INSTRUKCJE: 1. Wyodrębnij każdą osobę/użytkownika z tekstu 2. Dla każdego użytkownika zidentyfikuj: - email (WYMAGANY - jeśli brak prawidłowego emaila, pomiń użytkownika) - imię i nazwisko (jeśli dostępne) - firma (dopasuj do listy dostępnych firm po nazwie, nawet częściowej) - rola: jeśli tekst zawiera słowa "admin", "administrator", "zarząd" przy danej osobie - ustaw role na "ADMIN", w przeciwnym razie "MEMBER" 3. Jeśli email jest niepoprawny (brak @), dodaj ostrzeżenie 4. Jeśli firma nie pasuje do żadnej z listy, ustaw company_id na null ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed ani po): {{ "analysis": "Krótki opis znalezionych danych (1-2 zdania po polsku)", "users": [ {{ "email": "adres@email.pl", "name": "Imię Nazwisko lub null", "company_id": 123, "company_name": "Nazwa dopasowanej firmy lub null", "role": "MEMBER", "warnings": [] }} ] }}""" AI_USER_IMAGE_PROMPT = """Jesteś asystentem systemu NordaBiz pomagającym administratorowi tworzyć konta użytkowników. ZADANIE: Przeanalizuj ten obraz (screenshot) i wyodrębnij informacje o użytkownikach. Szukaj: adresów email, imion i nazwisk, nazw firm, ról (admin/user). DOSTĘPNE FIRMY W SYSTEMIE (id: nazwa): {companies_json} INSTRUKCJE: 1. Przeczytaj cały tekst widoczny na obrazie 2. Wyodrębnij każdą osobę/użytkownika 3. Dla każdego użytkownika zidentyfikuj: - email (WYMAGANY - jeśli brak, pomiń) - imię i nazwisko - firma (dopasuj do listy) - rola: admin lub zwykły użytkownik 4. Jeśli email jest nieczytelny lub niepoprawny, dodaj ostrzeżenie ZWRÓĆ TYLKO CZYSTY JSON w dokładnie takim formacie (bez żadnego tekstu przed ani po): {{ "analysis": "Krótki opis co widzisz na obrazie (1-2 zdania po polsku)", "users": [ {{ "email": "adres@email.pl", "name": "Imię Nazwisko lub null", "company_id": 123, "company_name": "Nazwa dopasowanej firmy lub null", "role": "MEMBER", "warnings": [] }} ] }}""" # ============================================================ # API ROUTES # ============================================================ @bp.route('/users-api/ai-parse', methods=['POST']) @login_required @role_required(SystemRole.ADMIN) def admin_users_ai_parse(): """Parse text or image with AI to extract user data.""" db = SessionLocal() try: # Get list of companies for AI context companies = db.query(Company).order_by(Company.name).all() companies_json = "\n".join([f"{c.id}: {c.name}" for c in companies]) # Check input type input_type = request.form.get('input_type') or (request.get_json() or {}).get('input_type', 'text') if input_type == 'image': # Handle image upload if 'file' not in request.files: return jsonify({'success': False, 'error': 'Brak pliku obrazu'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'success': False, 'error': 'Nie wybrano pliku'}), 400 # Validate file type allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'webp'} ext = file.filename.rsplit('.', 1)[-1].lower() if '.' in file.filename else '' if ext not in allowed_extensions: return jsonify({'success': False, 'error': 'Dozwolone formaty: PNG, JPG, JPEG, GIF, WEBP'}), 400 # Save temp file with tempfile.NamedTemporaryFile(delete=False, suffix=f'.{ext}') as tmp: file.save(tmp.name) temp_path = tmp.name try: # Get Gemini service and analyze image service = gemini_service.get_gemini_service() prompt = AI_USER_IMAGE_PROMPT.format(companies_json=companies_json) ai_response = service.analyze_image(temp_path, prompt) finally: # Clean up temp file if os.path.exists(temp_path): os.unlink(temp_path) else: # Handle text input data = request.get_json() or {} content = data.get('content', '').strip() if not content: return jsonify({'success': False, 'error': 'Brak treści do analizy'}), 400 # Get Gemini service and analyze text service = gemini_service.get_gemini_service() prompt = AI_USER_PARSE_PROMPT.format( input_text=content, companies_json=companies_json ) ai_response = service.generate_text( prompt=prompt, feature='ai_user_parse', user_id=current_user.id, temperature=0.3 # Lower temperature for more consistent JSON output ) # Parse AI response as JSON # Try to extract JSON from response (handle potential markdown code blocks) json_match = re.search(r'\{[\s\S]*\}', ai_response) if not json_match: logger.error(f"AI response not valid JSON: {ai_response[:500]}") return jsonify({ 'success': False, 'error': 'AI nie zwróciło prawidłowej odpowiedzi. Spróbuj ponownie.' }), 500 try: parsed = json.loads(json_match.group()) except json.JSONDecodeError as e: logger.error(f"JSON parse error: {e}, response: {ai_response[:500]}") return jsonify({ 'success': False, 'error': 'Błąd parsowania odpowiedzi AI. Spróbuj ponownie.' }), 500 # Check for duplicate emails in database proposed_users = parsed.get('users', []) existing_emails = [] for user in proposed_users: email = user.get('email', '').strip().lower() if email: existing = db.query(User).filter(User.email == email).first() if existing: existing_emails.append(email) user['warnings'] = user.get('warnings', []) + [f'Email już istnieje w systemie'] logger.info(f"Admin {current_user.email} used AI to parse users: {len(proposed_users)} found") return jsonify({ 'success': True, 'ai_response': parsed.get('analysis', 'Analiza zakończona'), 'proposed_users': proposed_users, 'duplicate_emails': existing_emails }) except Exception as e: logger.error(f"Error in AI user parse: {e}") return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500 finally: db.close() @bp.route('/users-api/bulk-create', methods=['POST']) @login_required @role_required(SystemRole.ADMIN) def admin_users_bulk_create(): """Create multiple users from confirmed proposals.""" db = SessionLocal() try: data = request.get_json() or {} users_to_create = data.get('users', []) if not users_to_create: return jsonify({'success': False, 'error': 'Brak użytkowników do utworzenia'}), 400 created = [] failed = [] password_chars = string.ascii_letters + string.digits + "!@#$%^&*" for user_data in users_to_create: email = user_data.get('email', '').strip().lower() if not email: failed.append({'email': email or 'brak', 'error': 'Brak adresu email'}) continue # Check if email already exists existing = db.query(User).filter(User.email == email).first() if existing: failed.append({'email': email, 'error': 'Email już istnieje'}) continue # Validate company_id if provided company_id = user_data.get('company_id') if company_id: company = db.query(Company).filter(Company.id == company_id).first() if not company: company_id = None # Reset if company doesn't exist # Generate password generated_password = ''.join(secrets.choice(password_chars) for _ in range(16)) password_hash = generate_password_hash(generated_password, method='pbkdf2:sha256') # Create user try: new_user = User( email=email, password_hash=password_hash, name=user_data.get('name', '').strip() or None, company_id=company_id, is_verified=True, is_active=True ) db.add(new_user) # Set role based on AI parse result (supports both old is_admin and new role field) ai_role = user_data.get('role', 'MEMBER') if ai_role == 'ADMIN' or user_data.get('is_admin', False): new_user.set_role(SystemRole.ADMIN) db.flush() # Get the ID created.append({ 'email': email, 'user_id': new_user.id, 'name': new_user.name, 'generated_password': generated_password }) except Exception as e: failed.append({'email': email, 'error': str(e)}) # Commit all successful creates if created: db.commit() logger.info(f"Admin {current_user.email} bulk created {len(created)} users via AI") return jsonify({ 'success': True, 'created': created, 'failed': failed, 'message': f'Utworzono {len(created)} użytkowników' + (f', {len(failed)} błędów' if failed else '') }) except Exception as e: db.rollback() logger.error(f"Error in bulk user create: {e}") return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500 finally: db.close() @bp.route('/users-api/change-role', methods=['POST']) @login_required @role_required(SystemRole.ADMIN) def admin_users_change_role(): """Change user's system role.""" db = SessionLocal() try: data = request.get_json() or {} user_id = data.get('user_id') new_role = data.get('role') if not user_id or not new_role: return jsonify({'success': False, 'error': 'Brak wymaganych danych'}), 400 # Validate role valid_roles = ['UNAFFILIATED', 'MEMBER', 'EMPLOYEE', 'MANAGER', 'OFFICE_MANAGER', 'ADMIN'] if new_role not in valid_roles: return jsonify({'success': False, 'error': f'Nieprawidłowa rola: {new_role}'}), 400 # Get user user = db.query(User).filter(User.id == user_id).first() if not user: return jsonify({'success': False, 'error': 'Użytkownik nie znaleziony'}), 404 # Prevent self-demotion from admin if user.id == current_user.id and new_role != 'ADMIN': return jsonify({'success': False, 'error': 'Nie możesz odebrać sobie uprawnień administratora'}), 400 old_role = user.role user.role = new_role # Sync is_admin flag user.is_admin = (new_role == 'ADMIN') # Update company_role based on new role if new_role in ['MANAGER']: user.company_role = 'MANAGER' elif new_role in ['EMPLOYEE']: user.company_role = 'EMPLOYEE' elif new_role in ['UNAFFILIATED', 'MEMBER']: user.company_role = 'NONE' # OFFICE_MANAGER and ADMIN keep their company_role unchanged # Create default permissions for EMPLOYEE if they have a company if new_role == 'EMPLOYEE' and user.company_id: existing_perms = db.query(UserCompanyPermissions).filter_by( user_id=user.id, company_id=user.company_id ).first() if not existing_perms: perms = UserCompanyPermissions( user_id=user.id, company_id=user.company_id, granted_by_id=current_user.id ) db.add(perms) db.commit() logger.info(f"Admin {current_user.email} changed role for user {user.email}: {old_role} -> {new_role}") return jsonify({ 'success': True, 'message': f'Rola zmieniona na {new_role}', 'user_id': user.id, 'old_role': old_role, 'new_role': new_role }) except Exception as e: db.rollback() logger.error(f"Error changing user role: {e}") return jsonify({'success': False, 'error': f'Błąd: {str(e)}'}), 500 finally: db.close() @bp.route('/users-api/roles', methods=['GET']) @login_required @role_required(SystemRole.ADMIN) def admin_users_get_roles(): """Get list of available roles for dropdown.""" roles = [ {'value': 'UNAFFILIATED', 'label': 'Niezrzeszony', 'description': 'Firma spoza Izby'}, {'value': 'MEMBER', 'label': 'Członek', 'description': 'Członek Norda bez firmy'}, {'value': 'EMPLOYEE', 'label': 'Pracownik', 'description': 'Pracownik firmy członkowskiej'}, {'value': 'MANAGER', 'label': 'Kadra Zarządzająca', 'description': 'Pełna kontrola firmy'}, {'value': 'OFFICE_MANAGER', 'label': 'Kierownik Biura', 'description': 'Panel admina bez użytkowników'}, {'value': 'ADMIN', 'label': 'Administrator', 'description': 'Pełne prawa'}, ] return jsonify({'success': True, 'roles': roles})