2026-08-14 18:02:25 +02:00
|
|
|
"""
|
|
|
|
|
API REST v1 - GMAO Collège
|
|
|
|
|
Endpoints publics avec authentification par clé API.
|
|
|
|
|
"""
|
|
|
|
|
from flask import Blueprint, request, jsonify, current_app
|
|
|
|
|
from flask_login import login_required, current_user
|
|
|
|
|
from functools import wraps
|
2026-08-15 00:56:36 +02:00
|
|
|
from werkzeug.security import check_password_hash
|
2026-08-14 18:02:25 +02:00
|
|
|
from app_new.extensions import db
|
|
|
|
|
|
|
|
|
|
api_bp = Blueprint('api_v1', __name__, url_prefix='/api/v1')
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 18:46:19 +02:00
|
|
|
def api_key_required(permission=None):
|
2026-08-14 18:02:25 +02:00
|
|
|
"""Authentification par clé API (header X-API-Key) ou session login."""
|
2026-08-21 18:46:19 +02:00
|
|
|
def decorator(f):
|
|
|
|
|
@wraps(f)
|
|
|
|
|
def decorated(*args, **kwargs):
|
2026-08-14 18:02:25 +02:00
|
|
|
# Accepter soit un header X-API-Key, soit une session Flask-Login
|
|
|
|
|
api_key = request.headers.get('X-API-Key')
|
|
|
|
|
if api_key:
|
|
|
|
|
from app_new.core.models.settings import AppSettings
|
2026-08-15 00:56:36 +02:00
|
|
|
api_key_hash = AppSettings.get('api_key_hash')
|
|
|
|
|
if api_key_hash and check_password_hash(api_key_hash, api_key):
|
2026-08-21 18:46:19 +02:00
|
|
|
# La clé historique reste une clé de service à accès complet.
|
2026-08-14 18:02:25 +02:00
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
return jsonify({'error': 'Clé API invalide'}), 401
|
|
|
|
|
# Sinon, fallback sur session login
|
2026-08-21 18:46:19 +02:00
|
|
|
if not current_user.is_authenticated:
|
|
|
|
|
return jsonify({'error': 'Authentification requise (X-API-Key ou login)'}), 401
|
|
|
|
|
from app_new.core.authorization import has_permission
|
|
|
|
|
if permission and not has_permission(permission, current_user):
|
|
|
|
|
return jsonify({'error': 'Permission insuffisante', 'permission': permission}), 403
|
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
return decorated
|
|
|
|
|
return decorator
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route('/status')
|
2026-08-21 18:46:19 +02:00
|
|
|
@api_key_required('dashboard.view')
|
2026-08-14 18:02:25 +02:00
|
|
|
def status():
|
|
|
|
|
"""État global du système."""
|
|
|
|
|
from app_new.core.models.maintenance import Intervention
|
|
|
|
|
from app_new.core.models.equipment import Equipment
|
|
|
|
|
from app_new.core.models.college import Room, Building
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'interventions_total': Intervention.query.count(),
|
|
|
|
|
'interventions_en_cours': Intervention.query.filter_by(status='en_cours').count(),
|
|
|
|
|
'interventions_en_attente': Intervention.query.filter_by(status='en_attente').count(),
|
|
|
|
|
'equipments_total': Equipment.query.count(),
|
2026-08-15 00:56:36 +02:00
|
|
|
'equipments_panne': Equipment.query.filter(Equipment.status.in_(['en_panne', 'hors_service', 'hs'])).count(),
|
2026-08-14 18:02:25 +02:00
|
|
|
'rooms_total': Room.query.count(),
|
|
|
|
|
'buildings_total': Building.query.count(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route('/interventions')
|
2026-08-21 18:46:19 +02:00
|
|
|
@api_key_required('intervention.view')
|
2026-08-14 18:02:25 +02:00
|
|
|
def list_interventions():
|
|
|
|
|
"""Liste des interventions avec pagination."""
|
|
|
|
|
from app_new.core.models.maintenance import Intervention
|
|
|
|
|
|
2026-08-15 00:56:36 +02:00
|
|
|
page = max(request.args.get('page', 1, type=int) or 1, 1)
|
|
|
|
|
per_page = min(max(request.args.get('per_page', 20, type=int) or 20, 1), 100)
|
2026-08-14 18:02:25 +02:00
|
|
|
status = request.args.get('status')
|
|
|
|
|
|
|
|
|
|
query = Intervention.query
|
|
|
|
|
if status:
|
|
|
|
|
query = query.filter_by(status=status)
|
|
|
|
|
|
|
|
|
|
total = query.count()
|
|
|
|
|
items = query.order_by(Intervention.created_at.desc())\
|
|
|
|
|
.offset((page - 1) * per_page)\
|
|
|
|
|
.limit(per_page).all()
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'total': total,
|
|
|
|
|
'page': page,
|
|
|
|
|
'per_page': per_page,
|
|
|
|
|
'items': [{
|
|
|
|
|
'id': i.id,
|
|
|
|
|
'title': i.title,
|
|
|
|
|
'description': (i.description or '')[:200],
|
|
|
|
|
'type': i.type,
|
|
|
|
|
'status': i.status,
|
|
|
|
|
'priority': i.priority,
|
|
|
|
|
'created_at': i.created_at.isoformat() if i.created_at else None,
|
|
|
|
|
'equipment_id': i.equipment_id,
|
|
|
|
|
'room_id': i.room_id,
|
|
|
|
|
} for i in items]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route('/interventions/<int:intervention_id>')
|
2026-08-21 18:46:19 +02:00
|
|
|
@api_key_required('intervention.view')
|
2026-08-14 18:02:25 +02:00
|
|
|
def get_intervention(intervention_id):
|
|
|
|
|
"""Détail d'une intervention."""
|
|
|
|
|
from app_new.core.models.maintenance import Intervention
|
|
|
|
|
|
|
|
|
|
intervention = Intervention.query.get_or_404(intervention_id)
|
|
|
|
|
return jsonify({
|
|
|
|
|
'id': intervention.id,
|
|
|
|
|
'title': intervention.title,
|
|
|
|
|
'description': intervention.description,
|
|
|
|
|
'type': intervention.type,
|
|
|
|
|
'status': intervention.status,
|
|
|
|
|
'priority': intervention.priority,
|
|
|
|
|
'created_at': intervention.created_at.isoformat() if intervention.created_at else None,
|
|
|
|
|
'started_at': intervention.started_at.isoformat() if intervention.started_at else None,
|
|
|
|
|
'completed_at': intervention.completed_at.isoformat() if intervention.completed_at else None,
|
|
|
|
|
'equipment_id': intervention.equipment_id,
|
|
|
|
|
'room_id': intervention.room_id,
|
|
|
|
|
'company_id': intervention.company_id,
|
|
|
|
|
'assigned_to_id': intervention.assigned_to_id,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route('/equipments')
|
2026-08-21 18:46:19 +02:00
|
|
|
@api_key_required('patrimoine.view')
|
2026-08-14 18:02:25 +02:00
|
|
|
def list_equipments():
|
|
|
|
|
"""Liste des équipements avec pagination."""
|
|
|
|
|
from app_new.core.models.equipment import Equipment
|
|
|
|
|
|
2026-08-15 00:56:36 +02:00
|
|
|
page = max(request.args.get('page', 1, type=int) or 1, 1)
|
|
|
|
|
per_page = min(max(request.args.get('per_page', 20, type=int) or 20, 1), 100)
|
2026-08-14 18:02:25 +02:00
|
|
|
status = request.args.get('status')
|
|
|
|
|
|
|
|
|
|
query = Equipment.query
|
|
|
|
|
if status:
|
|
|
|
|
query = query.filter_by(status=status)
|
|
|
|
|
|
|
|
|
|
total = query.count()
|
|
|
|
|
items = query.order_by(Equipment.name)\
|
|
|
|
|
.offset((page - 1) * per_page)\
|
|
|
|
|
.limit(per_page).all()
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
'total': total,
|
|
|
|
|
'page': page,
|
|
|
|
|
'per_page': per_page,
|
|
|
|
|
'items': [{
|
|
|
|
|
'id': e.id,
|
|
|
|
|
'name': e.name,
|
|
|
|
|
'status': e.status,
|
|
|
|
|
'category_id': e.category_id,
|
|
|
|
|
'room_id': e.room_id,
|
|
|
|
|
} for e in items]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@api_bp.route('/equipments/<int:equipment_id>')
|
2026-08-21 18:46:19 +02:00
|
|
|
@api_key_required('patrimoine.view')
|
2026-08-14 18:02:25 +02:00
|
|
|
def get_equipment(equipment_id):
|
|
|
|
|
"""Détail d'un équipement."""
|
|
|
|
|
from app_new.core.models.equipment import Equipment
|
|
|
|
|
|
|
|
|
|
equipment = Equipment.query.get_or_404(equipment_id)
|
|
|
|
|
return jsonify({
|
|
|
|
|
'id': equipment.id,
|
|
|
|
|
'name': equipment.name,
|
|
|
|
|
'status': equipment.status,
|
|
|
|
|
'serial_number': getattr(equipment, 'serial_number', None),
|
|
|
|
|
'category_id': equipment.category_id,
|
|
|
|
|
'room_id': equipment.room_id,
|
|
|
|
|
'description': getattr(equipment, 'description', None),
|
|
|
|
|
})
|