transfer from private server/git server

This commit is contained in:
M1n-0
2026-08-21 09:12:02 +02:00
parent 45c8982178
commit 6ecb014238
75 changed files with 4344 additions and 274 deletions

13
web/models/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
"""
Couche models/ — Repository Pattern (Bloc 1 - C4, Bloc 2 - C4).
Chaque module de ce package encapsule TOUT l'accès SQL pour une entité :
les couches routes/ et services/ n'écrivent jamais de SQL elles-mêmes et
ne connaissent pas le schéma de la base. Cela garantit que le changement
de SGBDR (ex. migration vers PostgreSQL) ne nécessite que la réécriture de
ce package — le reste de l'application est inchangé (dépendance vers
l'abstraction, cf. dossier technique section C3/C4).
Chaque module expose une interface CRUD standardisée :
insert(db, ...) / fetch_all(db, ...) / get(db, id) / update(db, ...) / delete(db, id)
"""

View File

@@ -0,0 +1,57 @@
"""
annonce_model.py — Accès aux données pour l'entité "annonce".
Toutes les requêtes utilisent des paramètres préparés (%s) : aucune
concaténation de chaîne SQL n'est utilisée dans ce module (OWASP A03:2021 —
Injection). Voir tests de sécurité TS-01 (tests/test_security.py).
"""
class AnnonceNotFoundError(Exception):
"""Exception métier levée quand une annonce n'existe pas.
La couche services/ traduit les erreurs SQL bas niveau en exceptions
métier compréhensibles, afin que les routes n'aient jamais à connaître
les détails de pymysql (cf. C4 — convention de gestion des erreurs).
"""
def insert(db, titre, contenu, auteur_sub, epinglee=False):
query = (
"INSERT INTO annonces (titre, contenu, epinglee, auteur_sub) "
"VALUES (%s, %s, %s, %s)"
)
return db.execute(query, (titre, contenu, int(epinglee), auteur_sub))
def fetch_all(db):
query = (
"SELECT id, titre, contenu, epinglee, auteur_sub, created_at, updated_at "
"FROM annonces ORDER BY epinglee DESC, created_at DESC"
)
return db.fetch_all(query)
def get(db, annonce_id):
query = (
"SELECT id, titre, contenu, epinglee, auteur_sub, created_at, updated_at "
"FROM annonces WHERE id = %s"
)
return db.fetch_one(query, (annonce_id,))
def update(db, annonce_id, titre, contenu, epinglee=False):
query = (
"UPDATE annonces SET titre = %s, contenu = %s, epinglee = %s "
"WHERE id = %s"
)
with db.cursor(commit=True) as cur:
cur.execute(query, (titre, contenu, int(epinglee), annonce_id))
return cur.rowcount
def delete(db, annonce_id):
query = "DELETE FROM annonces WHERE id = %s"
with db.cursor(commit=True) as cur:
cur.execute(query, (annonce_id,))
return cur.rowcount

17
web/models/audit_model.py Normal file
View File

@@ -0,0 +1,17 @@
"""audit_model.py — Écriture dans la table de traçabilité audit_log (Bloc 1 - C10)."""
def insert(db, action, statut, ressource=None, utilisateur_sub=None, ip_address=None):
query = (
"INSERT INTO audit_log (action, ressource, statut, utilisateur_sub, ip_address) "
"VALUES (%s, %s, %s, %s, %s)"
)
return db.execute(query, (action, ressource, statut, utilisateur_sub, ip_address))
def fetch_recent(db, limit=100):
query = (
"SELECT id, action, ressource, statut, utilisateur_sub, ip_address, created_at "
"FROM audit_log ORDER BY created_at DESC LIMIT %s"
)
return db.fetch_all(query, (limit,))

59
web/models/block_model.py Normal file
View File

@@ -0,0 +1,59 @@
"""
block_model.py — Accès aux données pour l'entité "block" (widgets du dashboard).
Module historique de GestHub, migré de Flask-SQLAlchemy vers PyMySQL pour
rester cohérent avec le reste de la couche models/ (accès direct, sans ORM,
requêtes paramétrées).
"""
import json
def _row_to_dict(row):
if row is None:
return None
return {
"id": row["id"],
"type": row["block_type"],
"column": row["column_name"],
"position": row["position"],
"data": json.loads(row["data"]) if row["data"] else {},
}
def fetch_all(db):
query = (
"SELECT id, block_type, column_name, position, data FROM blocks "
"ORDER BY column_name, position"
)
rows = db.fetch_all(query)
return [_row_to_dict(r) for r in rows]
def insert(db, block_type, column_name, data, position=99):
query = (
"INSERT INTO blocks (block_type, column_name, position, data) "
"VALUES (%s, %s, %s, %s)"
)
new_id = db.execute(query, (block_type, column_name, position, json.dumps(data)))
return _row_to_dict({
"id": new_id,
"block_type": block_type,
"column_name": column_name,
"position": position,
"data": json.dumps(data),
})
def update_position(db, block_id, column_name, position):
query = "UPDATE blocks SET column_name = %s, position = %s WHERE id = %s"
with db.cursor(commit=True) as cur:
cur.execute(query, (column_name, position, block_id))
return cur.rowcount
def delete(db, block_id):
query = "DELETE FROM blocks WHERE id = %s"
with db.cursor(commit=True) as cur:
cur.execute(query, (block_id,))
return cur.rowcount

View File

@@ -0,0 +1,32 @@
"""evenement_model.py — Accès aux données pour l'entité "evenement" (planning)."""
def insert(db, titre, description, date_debut, date_fin, createur_sub):
query = (
"INSERT INTO evenements (titre, description, date_debut, date_fin, createur_sub) "
"VALUES (%s, %s, %s, %s, %s)"
)
return db.execute(query, (titre, description, date_debut, date_fin, createur_sub))
def fetch_all(db):
query = (
"SELECT id, titre, description, date_debut, date_fin, createur_sub, created_at "
"FROM evenements ORDER BY date_debut ASC"
)
return db.fetch_all(query)
def get(db, evenement_id):
query = (
"SELECT id, titre, description, date_debut, date_fin, createur_sub, created_at "
"FROM evenements WHERE id = %s"
)
return db.fetch_one(query, (evenement_id,))
def delete(db, evenement_id):
query = "DELETE FROM evenements WHERE id = %s"
with db.cursor(commit=True) as cur:
cur.execute(query, (evenement_id,))
return cur.rowcount

View File

@@ -0,0 +1,32 @@
"""fichier_model.py — Accès aux données pour l'entité "fichier"."""
def insert(db, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub):
query = (
"INSERT INTO fichiers (nom_original, nom_stocke, mime_type, taille_octets, uploader_sub) "
"VALUES (%s, %s, %s, %s, %s)"
)
return db.execute(query, (nom_original, nom_stocke, mime_type, taille_octets, uploader_sub))
def fetch_all(db):
query = (
"SELECT id, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub, created_at "
"FROM fichiers ORDER BY created_at DESC"
)
return db.fetch_all(query)
def get(db, fichier_id):
query = (
"SELECT id, nom_original, nom_stocke, mime_type, taille_octets, uploader_sub, created_at "
"FROM fichiers WHERE id = %s"
)
return db.fetch_one(query, (fichier_id,))
def delete(db, fichier_id):
query = "DELETE FROM fichiers WHERE id = %s"
with db.cursor(commit=True) as cur:
cur.execute(query, (fichier_id,))
return cur.rowcount

54
web/models/rgpd_model.py Normal file
View File

@@ -0,0 +1,54 @@
"""
rgpd_model.py — Anonymisation des données personnelles (droit à l'effacement,
RGPD art. 17). Bloc 1 - C10 : traçabilité, et Bloc 2 - C4 : la seule donnée
personnelle stockée dans l'application est le `sub` OIDC (Keycloak) — aucune
autre table ne duplique d'attribut d'identité.
`sub` est référencé dans quatre tables : annonces.auteur_sub,
fichiers.uploader_sub, evenements.createur_sub (toutes NOT NULL — la donnée
ne peut donc pas être mise à NULL sans casser la contrainte, d'où le choix
d'un pseudonyme irréversible) et audit_log.utilisateur_sub (NULLable, mais
pseudonymisée elle aussi par cohérence, pour garder la traçabilité du volume
d'actions sans conserver l'identité).
Toutes les mises à jour sont exécutées dans UNE seule transaction InnoDB
(un seul curseur `commit=True`) : soit les quatre tables sont mises à jour,
soit aucune ne l'est en cas d'erreur (cohérence, pas d'anonymisation partielle).
"""
def anonymize(db, target_sub, anon_value):
"""Remplace `target_sub` par `anon_value` dans toutes les tables qui le
référencent. Retourne le nombre de lignes modifiées par table.
"""
with db.cursor(commit=True) as cur:
cur.execute(
"UPDATE annonces SET auteur_sub = %s WHERE auteur_sub = %s",
(anon_value, target_sub),
)
annonces_rows = cur.rowcount
cur.execute(
"UPDATE fichiers SET uploader_sub = %s WHERE uploader_sub = %s",
(anon_value, target_sub),
)
fichiers_rows = cur.rowcount
cur.execute(
"UPDATE evenements SET createur_sub = %s WHERE createur_sub = %s",
(anon_value, target_sub),
)
evenements_rows = cur.rowcount
cur.execute(
"UPDATE audit_log SET utilisateur_sub = %s WHERE utilisateur_sub = %s",
(anon_value, target_sub),
)
audit_rows = cur.rowcount
return {
"annonces": annonces_rows,
"fichiers": fichiers_rows,
"evenements": evenements_rows,
"audit_log": audit_rows,
}