Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Replace ~20 remaining is_admin references across backend, templates and scripts with proper SystemRole checks. Column is_admin stays as deprecated (synced by set_role()) until DB migration removes it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
403 lines
14 KiB
Python
403 lines
14 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, 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})
|