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>
308 lines
11 KiB
Python
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()
|