nordabiz/blueprints/admin/routes_users_api.py
Maciej Pienczyn 49830855a2 refactor: Migrate ZOPK and Users API routes to admin blueprint
Major refactoring to reduce app.py size by ~22%:
- Move all ZOPK routes (47 endpoints) to 4 blueprint files:
  - routes_zopk_dashboard.py - main dashboard
  - routes_zopk_news.py - news management, scraping, AI evaluation
  - routes_zopk_knowledge.py - knowledge base, embeddings, graph
  - routes_zopk_timeline.py - milestones management
- Move Users API routes to routes_users_api.py:
  - /admin/users-api/ai-parse - AI-powered user parsing
  - /admin/users-api/bulk-create - bulk user creation
- Move notify-release to routes.py

app.py reduced from 11518 to 8916 lines (-22.6%)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 17:05:36 +01:00

308 lines
11 KiB
Python

"""
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
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 is_admin na true
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",
"is_admin": false,
"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",
"is_admin": false,
"warnings": []
}}
]
}}"""
# ============================================================
# API ROUTES
# ============================================================
@bp.route('/users-api/ai-parse', methods=['POST'])
@login_required
def admin_users_ai_parse():
"""Parse text or image with AI to extract user data."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
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
def admin_users_bulk_create():
"""Create multiple users from confirmed proposals."""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
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_admin=user_data.get('is_admin', False),
is_verified=True,
is_active=True
)
db.add(new_user)
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()