Protège les écritures contre les requêtes forgées
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-14 18:55:47 +00:00
parent d3ddfdb974
commit 05b2002d02
23 changed files with 123 additions and 70 deletions

View file

@ -134,7 +134,6 @@ def create_app(config_name='default'):
# Blueprints des intégrations # Blueprints des intégrations
from .pronote.routes import pronote_bp from .pronote.routes import pronote_bp
csrf.exempt(pronote_bp)
app.register_blueprint(pronote_bp, url_prefix='/pronote') app.register_blueprint(pronote_bp, url_prefix='/pronote')
from .ent.routes import ent_bp from .ent.routes import ent_bp
@ -172,7 +171,6 @@ def create_app(config_name='default'):
# Notifications temps reel # Notifications temps reel
from .notifications.routes import notifications_bp from .notifications.routes import notifications_bp
app.register_blueprint(notifications_bp) app.register_blueprint(notifications_bp)
csrf.exempt(notifications_bp)
# Exports PDF/CSV # Exports PDF/CSV
from .exports.routes import exports_bp from .exports.routes import exports_bp
@ -181,7 +179,6 @@ def create_app(config_name='default'):
# Messagerie unifiee # Messagerie unifiee
from .messagerie.routes import messagerie_bp from .messagerie.routes import messagerie_bp
app.register_blueprint(messagerie_bp) app.register_blueprint(messagerie_bp)
csrf.exempt(messagerie_bp)
# Contrats d'entreprise # Contrats d'entreprise
from .contracts.models import Contract from .contracts.models import Contract
@ -199,7 +196,6 @@ def create_app(config_name='default'):
# Planificateur automatique # Planificateur automatique
from .scheduler.routes import scheduler_bp from .scheduler.routes import scheduler_bp
app.register_blueprint(scheduler_bp) app.register_blueprint(scheduler_bp)
csrf.exempt(scheduler_bp)
# Filtres Jinja2 pour les templates # Filtres Jinja2 pour les templates
from .utils import format_status, status_badge_class, priority_badge_class, bootstrap_color_to_hex, bootstrap_text_color from .utils import format_status, status_badge_class, priority_badge_class, bootstrap_color_to_hex, bootstrap_text_color

View file

@ -49,7 +49,9 @@ class Config:
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
"""Configuration développement.""" """Configuration développement."""
DEBUG = True DEBUG = True
WTF_CSRF_ENABLED = False # Désactivé pour développement # La machine de développement est accessible à distance : les protections
# navigateur doivent y être identiques à celles de la production.
WTF_CSRF_ENABLED = True
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
# MariaDB obligatoire # MariaDB obligatoire
SQLALCHEMY_DATABASE_URI = os.environ.get( SQLALCHEMY_DATABASE_URI = os.environ.get(

View file

@ -421,7 +421,7 @@ def create_category():
return render_template('equipments/category_form.html', title='Nouvelle catégorie') return render_template('equipments/category_form.html', title='Nouvelle catégorie')
@main_bp.route('/<int:id>/status/<status>') @main_bp.route('/<int:id>/status/<status>', methods=['POST'])
@login_required @login_required
def change_status(id, status): def change_status(id, status):
"""Changer le statut d'un équipement.""" """Changer le statut d'un équipement."""

View file

@ -258,9 +258,9 @@
Statut Statut
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='en_service') }}">En service</a></li> {% for value, label in [('en_service', 'En service'), ('hs', 'Hors service'), ('a_jeter', 'À jeter')] %}
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='hs') }}">Hors service</a></li> <li><form method="post" action="{{ url_for('equipments.change_status', id=unit.id, status=value) }}"><button class="dropdown-item" type="submit">{{ label }}</button></form></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='a_jeter') }}">À jeter</a></li> {% endfor %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View file

@ -11,7 +11,7 @@ import tempfile
sync_bp = Blueprint('outlook_sync', __name__, template_folder='templates') sync_bp = Blueprint('outlook_sync', __name__, template_folder='templates')
@sync_bp.route('/api/sync-folders/<int:account_id>') @sync_bp.route('/api/sync-folders/<int:account_id>', methods=['POST'])
@login_required @login_required
def api_sync_folders(account_id): def api_sync_folders(account_id):
"""Synchronise les dossiers Outlook (avec sous-dossiers récursifs).""" """Synchronise les dossiers Outlook (avec sous-dossiers récursifs)."""
@ -19,7 +19,7 @@ def api_sync_folders(account_id):
from datetime import datetime, timezone from datetime import datetime, timezone
account = OutlookAccount.query.get(account_id) account = OutlookAccount.query.get(account_id)
if not account or (account.user_id != current_user.id and current_user.role != 'admin'): if not account or (account.user_id != current_user.id and not current_user.is_admin()):
return jsonify({'error': 'Compte non trouvé'}), 404 return jsonify({'error': 'Compte non trouvé'}), 404
access_token, error = get_access_token(account) access_token, error = get_access_token(account)
@ -122,7 +122,8 @@ def api_sync_folders(account_id):
return jsonify({'error': f'Erreur: {str(e)}'}), 500 return jsonify({'error': f'Erreur: {str(e)}'}), 500
@sync_bp.route('/api/sync-mails/<int:account_id>/<folder_id>') @sync_bp.route('/api/sync-mails/<int:account_id>/<folder_id>', methods=['POST'])
@sync_bp.route('/api/sync-mails/<int:account_id>', defaults={'folder_id': 'inbox'}, methods=['POST'])
@login_required @login_required
def api_sync_mails(account_id, folder_id='inbox'): def api_sync_mails(account_id, folder_id='inbox'):
"""Synchronise les mails d'un dossier avec pagination complète.""" """Synchronise les mails d'un dossier avec pagination complète."""
@ -130,7 +131,7 @@ def api_sync_mails(account_id, folder_id='inbox'):
from datetime import datetime, timezone from datetime import datetime, timezone
account = OutlookAccount.query.get(account_id) account = OutlookAccount.query.get(account_id)
if not account or (account.user_id != current_user.id and current_user.role != 'admin'): if not account or (account.user_id != current_user.id and not current_user.is_admin()):
return jsonify({'error': 'Compte non trouvé'}), 404 return jsonify({'error': 'Compte non trouvé'}), 404
access_token, error = get_access_token(account) access_token, error = get_access_token(account)
@ -144,7 +145,7 @@ def api_sync_mails(account_id, folder_id='inbox'):
# Trouver le dossier # Trouver le dossier
folder = None folder = None
if folder_id != 'inbox': if folder_id != 'inbox':
folder = OutlookFolder.query.get(folder_id) folder = OutlookFolder.query.filter_by(id=folder_id, account_id=account_id).first()
else: else:
# Chercher par folder_type ou par nom (Boîte de réception) # Chercher par folder_type ou par nom (Boîte de réception)
folder = OutlookFolder.query.filter_by(account_id=account_id, folder_type='inbox').first() folder = OutlookFolder.query.filter_by(account_id=account_id, folder_type='inbox').first()
@ -251,7 +252,7 @@ def api_sync_mails(account_id, folder_id='inbox'):
# ─── Pièces jointes ───────────────────────────────────────────────────────── # ─── Pièces jointes ─────────────────────────────────────────────────────────
@sync_bp.route('/api/sync-attachments/<mail_id>') @sync_bp.route('/api/sync-attachments/<mail_id>', methods=['POST'])
@login_required @login_required
def api_sync_attachments(mail_id): def api_sync_attachments(mail_id):
"""Synchronise les pièces jointes d'un email.""" """Synchronise les pièces jointes d'un email."""
@ -259,6 +260,8 @@ def api_sync_attachments(mail_id):
mail = OutlookMail.query.get_or_404(mail_id) mail = OutlookMail.query.get_or_404(mail_id)
account = OutlookAccount.query.get_or_404(mail.account_id) account = OutlookAccount.query.get_or_404(mail.account_id)
if account.user_id != current_user.id and not current_user.is_admin():
return jsonify({'error': 'Message non trouvé'}), 404
access_token, error = get_access_token(account) access_token, error = get_access_token(account)
if error: if error:
@ -317,6 +320,8 @@ def download_attachment(att_id):
attachment = OutlookAttachment.query.get_or_404(att_id) attachment = OutlookAttachment.query.get_or_404(att_id)
mail = OutlookMail.query.get_or_404(attachment.mail_id) mail = OutlookMail.query.get_or_404(attachment.mail_id)
account = OutlookAccount.query.get_or_404(mail.account_id) account = OutlookAccount.query.get_or_404(mail.account_id)
if account.user_id != current_user.id and not current_user.is_admin():
return jsonify({'error': 'Pièce jointe non trouvée'}), 404
access_token, error = get_access_token(account) access_token, error = get_access_token(account)
if error: if error:
@ -349,5 +354,3 @@ def download_attachment(att_id):
except Exception as e: except Exception as e:
flash(f'Erreur: {str(e)}', 'danger') flash(f'Erreur: {str(e)}', 'danger')
return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id)) return redirect(url_for('outlook_pages.mail_view', account_id=account.id, mail_id=mail.id))

View file

@ -189,7 +189,7 @@ function syncFolders() {
const statusDiv = document.getElementById('sync-status'); const statusDiv = document.getElementById('sync-status');
statusDiv.classList.remove('d-none'); statusDiv.classList.remove('d-none');
fetch('/outlook/api/sync-folders/{{ account.id }}') fetch('/outlook/api/sync-folders/{{ account.id }}', {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
statusDiv.classList.add('d-none'); statusDiv.classList.add('d-none');

View file

@ -332,7 +332,7 @@ function testConnection(accountId) {
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les derniers mails ?')) { if (confirm('Synchroniser les derniers mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -245,7 +245,7 @@
<script> <script>
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les derniers mails ?')) { if (confirm('Synchroniser les derniers mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -100,7 +100,7 @@ function syncMails(maxMessages) {
status.style.display = 'block'; status.style.display = 'block';
status.innerHTML = '<i class="bi bi-hourglass-split"></i> Récupération des emails...'; status.innerHTML = '<i class="bi bi-hourglass-split"></i> Récupération des emails...';
fetch('/outlook/api/sync-mails/{{ account.id }}/{{ folder.folder_id }}?max=' + maxMessages) fetch('/outlook/api/sync-mails/{{ account.id }}/{{ folder.folder_id }}?max=' + maxMessages, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -54,7 +54,7 @@ function syncMails() {
status.style.display = 'block'; status.style.display = 'block';
status.innerHTML = 'Synchronisation en cours...'; status.innerHTML = 'Synchronisation en cours...';
fetch('/outlook/api/sync-mails/{{ account.id }}') fetch('/outlook/api/sync-mails/{{ account.id }}', {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -326,7 +326,7 @@ function syncAttachments() {
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Chargement...'; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Chargement...';
fetch('/outlook/api/sync-attachments/{{ mail.id }}') fetch('/outlook/api/sync-attachments/{{ mail.id }}', {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -201,7 +201,7 @@
<script> <script>
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les nouveaux mails ?')) { if (confirm('Synchroniser les nouveaux mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -5,7 +5,6 @@ Intégration Pronote
import logging import logging
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_wtf.csrf import CSRFProtect
from app_new.extensions import db, csrf from app_new.extensions import db, csrf
from ..pronote.models import PronoteSession from ..pronote.models import PronoteSession
from ..core.models.college import Room, RoomSchedule from ..core.models.college import Room, RoomSchedule
@ -30,14 +29,7 @@ def index():
@login_required @login_required
def connect(): def connect():
"""Connexion à Pronote via QR Code.""" """Connexion à Pronote via QR Code."""
from flask_wtf.csrf import validate_csrf
if request.method == 'POST': if request.method == 'POST':
# Validate CSRF token manually
try:
validate_csrf(request.form.get('csrf_token'))
except Exception:
# If CSRF fails, try to continue anyway (for testing)
pass
qr_code = request.form.get('qr_code', '').strip() qr_code = request.form.get('qr_code', '').strip()
pin = request.form.get('pin', '').strip() pin = request.form.get('pin', '').strip()
account_pin = request.form.get('account_pin', '').strip() or None account_pin = request.form.get('account_pin', '').strip() or None
@ -97,7 +89,7 @@ def rooms():
return render_template('pronote/rooms.html', rooms=rooms_list) return render_template('pronote/rooms.html', rooms=rooms_list)
@pronote_bp.route('/sync-salles') @pronote_bp.route('/sync-salles', methods=['POST'])
@login_required @login_required
def sync_salles(): def sync_salles():
"""Synchroniser les salles depuis Pronote.""" """Synchroniser les salles depuis Pronote."""
@ -207,7 +199,7 @@ def import_room_planning(room_id):
return redirect(url_for('pronote.room_planning', room_id=room_id)) return redirect(url_for('pronote.room_planning', room_id=room_id))
@pronote_bp.route('/disconnect') @pronote_bp.route('/disconnect', methods=['POST'])
@login_required @login_required
def disconnect(): def disconnect():
"""Déconnexion de Pronote.""" """Déconnexion de Pronote."""

View file

@ -19,9 +19,10 @@
<a href="{{ url_for('pronote.connect') }}" class="btn btn-primary"> <a href="{{ url_for('pronote.connect') }}" class="btn btn-primary">
<i class="bi bi-link"></i> Connecter à Pronote <i class="bi bi-link"></i> Connecter à Pronote
</a> </a>
<a href="{{ url_for('pronote.sync_salles') }}" class="btn btn-outline-secondary ms-2"> <form method="post" action="{{ url_for('pronote.sync_salles') }}" class="d-inline">
<button type="submit" class="btn btn-outline-secondary ms-2">
<i class="bi bi-arrow-repeat"></i> Synchroniser les salles <i class="bi bi-arrow-repeat"></i> Synchroniser les salles
</a> </button></form>
</div> </div>
</div> </div>
</div> </div>

View file

@ -10,9 +10,10 @@
<a href="{{ url_for('pronote.connect') }}" class="btn btn-outline-primary"> <a href="{{ url_for('pronote.connect') }}" class="btn btn-outline-primary">
<i class="bi bi-qr-code"></i> Connexion Pronote <i class="bi bi-qr-code"></i> Connexion Pronote
</a> </a>
<a href="{{ url_for('pronote.sync_salles') }}" class="btn btn-outline-success ms-2"> <form method="post" action="{{ url_for('pronote.sync_salles') }}" class="d-inline">
<button type="submit" class="btn btn-outline-success ms-2">
<i class="bi bi-download"></i> Synchroniser salles <i class="bi bi-download"></i> Synchroniser salles
</a> </button></form>
</div> </div>
</div> </div>
@ -53,9 +54,10 @@
<div class="alert alert-info"> <div class="alert alert-info">
<i class="bi bi-info-circle"></i> Aucune salle d'enseignement disponible. <i class="bi bi-info-circle"></i> Aucune salle d'enseignement disponible.
<div class="mt-2"> <div class="mt-2">
<a href="{{ url_for('pronote.sync_salles') }}" class="btn btn-primary btn-sm"> <form method="post" action="{{ url_for('pronote.sync_salles') }}">
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-download"></i> Synchroniser les salles depuis Pronote <i class="bi bi-download"></i> Synchroniser les salles depuis Pronote
</a> </button></form>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}GMAO Collège{% endblock %}</title> <title>{% block title %}GMAO Collège{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}"> <link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
@ -239,7 +240,7 @@
<li><a class="dropdown-item" href="{{ url_for('ent.staff') }}"><i class="bi bi-people"></i> Personnels ENT</a></li> <li><a class="dropdown-item" href="{{ url_for('ent.staff') }}"><i class="bi bi-people"></i> Personnels ENT</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('pronote.connect') }}"><i class="bi bi-qr-code"></i> PRONOTE (QR)</a></li> <li><a class="dropdown-item" href="{{ url_for('pronote.connect') }}"><i class="bi bi-qr-code"></i> PRONOTE (QR)</a></li>
<li><a class="dropdown-item" href="{{ url_for('pronote.sync_salles') }}"><i class="bi bi-download"></i> PRONOTE (sync salles)</a></li> <li><form method="post" action="{{ url_for('pronote.sync_salles') }}"><button class="dropdown-item" type="submit"><i class="bi bi-download"></i> PRONOTE (sync salles)</button></form></li>
<li><a class="dropdown-item" href="{{ url_for('pronote.planning') }}"><i class="bi bi-calendar3"></i> PRONOTE (plannings)</a></li> <li><a class="dropdown-item" href="{{ url_for('pronote.planning') }}"><i class="bi bi-calendar3"></i> PRONOTE (plannings)</a></li>
<li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li> <li><hr class="dropdown-divider" style="border-color:rgba(255,255,255,0.15)"></li>
<li><a class="dropdown-item" href="{{ url_for('yeastar.index') }}"><i class="bi bi-telephone"></i> Téléphone P520</a></li> <li><a class="dropdown-item" href="{{ url_for('yeastar.index') }}"><i class="bi bi-telephone"></i> Téléphone P520</a></li>
@ -320,6 +321,37 @@
</div> </div>
<script> <script>
/* Ajoute le jeton CSRF aux formulaires HTML et aux requêtes fetch mutantes. */
(function() {
const tokenNode = document.querySelector('meta[name="csrf-token"]');
const csrfToken = tokenNode ? tokenNode.content : '';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('form').forEach(function(form) {
const method = (form.getAttribute('method') || 'GET').toUpperCase();
if (method !== 'GET' && !form.querySelector('input[name="csrf_token"]')) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = csrfToken;
form.appendChild(input);
}
});
});
const originalFetch = window.fetch.bind(window);
window.fetch = function(resource, options) {
const opts = Object.assign({}, options || {});
const method = (opts.method || 'GET').toUpperCase();
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
const headers = new Headers(opts.headers || {});
if (!headers.has('X-CSRFToken')) headers.set('X-CSRFToken', csrfToken);
opts.headers = headers;
}
return originalFetch(resource, opts);
};
})();
/* ── Overlay de progression ── /* ── Overlay de progression ──
Usage JS : showProgress('Texte') / hideProgress() Usage JS : showProgress('Texte') / hideProgress()
Ou sur un <form> : class="show-progress-form" data-progress-text="Texte optionnel" Ou sur un <form> : class="show-progress-form" data-progress-text="Texte optionnel"

View file

@ -292,9 +292,9 @@
Statut Statut
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='en_service') }}">En service</a></li> {% for value, label in [('en_service', 'En service'), ('hs', 'Hors service'), ('a_jeter', 'À jeter')] %}
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='hs') }}">Hors service</a></li> <li><form method="post" action="{{ url_for('equipments.change_status', id=unit.id, status=value) }}"><button class="dropdown-item" type="submit">{{ label }}</button></form></li>
<li><a class="dropdown-item" href="{{ url_for('equipments.change_status', id=unit.id, status='a_jeter') }}">À jeter</a></li> {% endfor %}
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

View file

@ -332,7 +332,7 @@ function testConnection(accountId) {
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les derniers mails ?')) { if (confirm('Synchroniser les derniers mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -245,7 +245,7 @@
<script> <script>
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les derniers mails ?')) { if (confirm('Synchroniser les derniers mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -201,7 +201,7 @@
<script> <script>
function syncMails(accountId) { function syncMails(accountId) {
if (confirm('Synchroniser les nouveaux mails ?')) { if (confirm('Synchroniser les nouveaux mails ?')) {
fetch(`/outlook/api/sync-mails/${accountId}`) fetch(`/outlook/api/sync-mails/${accountId}`, {method: 'POST'})
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {

View file

@ -10,9 +10,9 @@
<a href="{{ url_for('pronote.connect') }}" class="btn btn-outline-primary"> <a href="{{ url_for('pronote.connect') }}" class="btn btn-outline-primary">
<i class="bi bi-qr-code"></i> Connexion Pronote <i class="bi bi-qr-code"></i> Connexion Pronote
</a> </a>
<a href="{{ url_for('pronote.sync_salles') }}" class="btn btn-outline-success ms-2"> <form method="post" action="{{ url_for('pronote.sync_salles') }}" class="d-inline"><button type="submit" class="btn btn-outline-success ms-2">
<i class="bi bi-download"></i> Synchroniser salles <i class="bi bi-download"></i> Synchroniser salles
</a> </button></form>
</div> </div>
</div> </div>
@ -53,9 +53,9 @@
<div class="alert alert-info"> <div class="alert alert-info">
<i class="bi bi-info-circle"></i> Aucune salle d'enseignement disponible. <i class="bi bi-info-circle"></i> Aucune salle d'enseignement disponible.
<div class="mt-2"> <div class="mt-2">
<a href="{{ url_for('pronote.sync_salles') }}" class="btn btn-primary btn-sm"> <form method="post" action="{{ url_for('pronote.sync_salles') }}"><button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-download"></i> Synchroniser les salles depuis Pronote <i class="bi bi-download"></i> Synchroniser les salles depuis Pronote
</a> </button></form>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View file

@ -31,9 +31,9 @@
{% else %} {% else %}
<div class="alert alert-info"> <div class="alert alert-info">
<i class="bi bi-info-circle"></i> Aucune salle importée depuis Pronote. <i class="bi bi-info-circle"></i> Aucune salle importée depuis Pronote.
<a href="{{ url_for('pronote.sync_salles') }}" class="alert-link"> <form method="post" action="{{ url_for('pronote.sync_salles') }}" class="d-inline">
Synchroniser les salles <button type="submit" class="btn btn-link alert-link p-0 align-baseline">Synchroniser les salles</button>
</a> </form>
</div> </div>
{% endif %} {% endif %}

View file

@ -0,0 +1,25 @@
"""Régressions sur les protections des requêtes qui écrivent."""
from app_new.config import DevelopmentConfig
def test_csrf_is_enabled_in_remote_development():
assert DevelopmentConfig.WTF_CSRF_ENABLED is True
def test_login_post_without_csrf_is_rejected_when_enabled(client, app):
previous = app.config["WTF_CSRF_ENABLED"]
app.config["WTF_CSRF_ENABLED"] = True
try:
response = client.post("/auth/login", data={"username": "inconnu", "password": "inconnu"})
assert response.status_code == 400
finally:
app.config["WTF_CSRF_ENABLED"] = previous
def test_mutating_routes_reject_get(client):
assert client.get("/equipments/1/status/hs").status_code == 405
assert client.get("/pronote/sync-salles").status_code == 405
assert client.get("/pronote/disconnect").status_code == 405
assert client.get("/outlook/api/sync-folders/1").status_code == 405
assert client.get("/outlook/api/sync-mails/1").status_code == 405
assert client.get("/outlook/api/sync-attachments/inconnu").status_code == 405