Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,29 @@ jobs:
- name: Run backend tests
run: ./scripts/test_backend.sh

backend-audit:
name: Backend dependency audit
runs-on: ubuntu-latest
needs:
- change-detection
if: needs.change-detection.outputs.backend == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: pip
cache-dependency-path: backend/requirements.txt

- name: Install pip-audit
run: python -m pip install --upgrade pip pip-audit

- name: Audit backend dependencies
run: python -m pip_audit -r backend/requirements.txt --strict

frontend-tests:
name: Frontend tests
runs-on: ubuntu-latest
Expand All @@ -146,6 +169,31 @@ jobs:
working-directory: frontend
run: npm test

frontend-audit:
name: Frontend dependency audit
runs-on: ubuntu-latest
needs:
- change-detection
if: needs.change-detection.outputs.frontend == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install frontend dependencies
working-directory: frontend
run: npm ci

- name: Audit frontend dependencies
working-directory: frontend
run: npm audit --audit-level=low

frontend-build:
name: Frontend build
runs-on: ubuntu-latest
Expand Down Expand Up @@ -177,9 +225,11 @@ jobs:
needs:
- change-detection
- backend-tests
- backend-audit
- frontend-tests
- frontend-audit
- frontend-build
if: startsWith(github.ref, 'refs/tags/') && needs.change-detection.outputs.age_secrets_image == 'true' && needs.backend-tests.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-build.result == 'success'
if: startsWith(github.ref, 'refs/tags/') && needs.change-detection.outputs.age_secrets_image == 'true' && needs.backend-tests.result == 'success' && needs.backend-audit.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-audit.result == 'success' && needs.frontend-build.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v5
Expand Down Expand Up @@ -215,9 +265,11 @@ jobs:
runs-on: ubuntu-latest
needs:
- backend-tests
- backend-audit
- frontend-tests
- frontend-audit
- frontend-build
if: startsWith(github.ref, 'refs/tags/') && needs.backend-tests.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-build.result == 'success'
if: startsWith(github.ref, 'refs/tags/') && needs.backend-tests.result == 'success' && needs.backend-audit.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-audit.result == 'success' && needs.frontend-build.result == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v5
Expand Down Expand Up @@ -268,10 +320,12 @@ jobs:
docker-images:
name: Build and publish Docker images
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/') && needs.backend-tests.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-build.result == 'success'
if: startsWith(github.ref, 'refs/tags/') && needs.backend-tests.result == 'success' && needs.backend-audit.result == 'success' && needs.frontend-tests.result == 'success' && needs.frontend-audit.result == 'success' && needs.frontend-build.result == 'success'
needs:
- backend-tests
- backend-audit
- frontend-tests
- frontend-audit
- frontend-build
steps:
- name: Checkout repository
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fonctionnels et techniques sont dans `documentation/` :
- [documentation/database.md](documentation/database.md) : schema PostgreSQL,
migrations et persistance production.
- [documentation/deploy.md](documentation/deploy.md) : deploiement, archive de
livraison, Docker Compose runtime et secrets production.
livraison, Docker Compose runtime, retours GitHub et secrets production.
- [documentation/authentication.md](documentation/authentication.md) :
authentification, profils et sessions.
- [documentation/register.md](documentation/register.md) : inscription et
Expand Down
3 changes: 3 additions & 0 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
AuthenticationController,
CollectionController,
CollectionShareController,
FeedbackController,
GameController,
LibraryController,
PlatformController,
Expand Down Expand Up @@ -110,6 +111,7 @@
auth_guard,
collection_share_management_service,
)
feedback_controller = FeedbackController(auth_guard)
user_controller = UserController(auth_guard)
collection_controller = CollectionController(auth_guard)
library_reset_job_coordinator = LibraryResetJobCoordinator()
Expand Down Expand Up @@ -159,6 +161,7 @@
authentication_controller.register_routes(app)
route_controller.register_routes(app)
collection_share_controller.register_routes(app)
feedback_controller.register_routes(app)
user_controller.register_routes(app)
user_collection_import_controller.register_routes(app)
user_collection_import_help_controller.register_routes(app)
Expand Down
2 changes: 2 additions & 0 deletions backend/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .authentication_controller import AuthenticationController
from .collection_controller import CollectionController
from .collection_share_controller import CollectionShareController
from .feedback_controller import FeedbackController
from .game_controller import GameController
from .library_controller import LibraryController
from .platform_controller import PlatformController
Expand All @@ -28,6 +29,7 @@
"AuthenticationController",
"CollectionController",
"CollectionShareController",
"FeedbackController",
"GameController",
"LibraryController",
"PlatformController",
Expand Down
80 changes: 80 additions & 0 deletions backend/controllers/feedback_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ____ _ _ ____ _ _ _ _ ___
# / ___| | ___ _ _ __| |/ ___|___ | | | ___ ___| |_(_) ___ _ __ / _ \ _ __ _ __
# | | | |/ _ \| | | |/ _` | | / _ \| | |/ _ \/ __| __| |/ _ \| `_ \| | | | `_ \| `_ |
# | |___| | (_) | |_| | (_| | |__| (_) | | | __/ (__| |_| | (_) | | | | |_| | |_) | |_) |
# \____|_|\___/ \__,_|\__,_|\____\___/|_|_|\___|\___|\__|_|\___/|_| |_|\___/| .__/| .__/
# |_| |_|
# Projet : CloudCollectionApp
# Date de creation : 2026-08-23
# Auteurs : OpenAI ChatGPT, Codex, Binda Sébastien
# Licence : Apache 2.0
#
# Description : controleur HTTP des retours beta utilisateurs.

from flask import Flask, current_app, jsonify, request

from services import AuthGuard, UserProfile
from services.feedback import GitHubFeedbackService


class FeedbackController:
"""Expose la route protegee d'envoi de retour beta vers GitHub."""

def __init__(self, auth_guard: AuthGuard, feedback_service_factory=None):
"""Initialise le controleur des retours.

Args:
auth_guard (AuthGuard): Garde d'authentification applicatif.
feedback_service_factory (Callable | None): Fabrique du service de retour.

Returns:
None: Le constructeur ne retourne aucune valeur.
"""

self.auth_guard = auth_guard
self.feedback_service_factory = feedback_service_factory or GitHubFeedbackService.from_environment

def register_routes(self, flask_app: Flask) -> None:
"""Enregistre la route de retour beta dans Flask.

Args:
flask_app (Flask): Application Flask cible.

Returns:
None: La methode ne retourne aucune valeur.
"""

flask_app.add_url_rule(
"/api/feedback",
endpoint="submit_feedback",
view_func=self.auth_guard.require_profile(UserProfile.USER.value)(self.submit_feedback),
methods=["POST"],
)

def submit_feedback(self):
"""Cree une issue GitHub depuis un retour utilisateur connecte.

Args:
Aucun.

Returns:
tuple[flask.Response, int]: Issue creee ou erreur JSON.
"""

payload = request.get_json(silent=True) or {}
if not isinstance(payload, dict):
return jsonify({"error": "Le format du retour est invalide."}), 400

requester_subject = str(
self.auth_guard.get_current_token_payload().get("sub") or ""
).strip().lower()
try:
feedback = self.feedback_service_factory().submit_feedback(payload, requester_subject)
return jsonify({"feedback": feedback}), 201
except ValueError as exc:
return jsonify({"error": str(exc)}), 400
except RuntimeError as exc:
return jsonify({"error": str(exc)}), 503
except Exception:
current_app.logger.exception("Erreur pendant l'envoi d'un retour beta.")
return jsonify({"error": "Unable to send feedback."}), 500
17 changes: 17 additions & 0 deletions backend/services/feedback/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ____ _ _ ____ _ _ _ _ ___
# / ___| | ___ _ _ __| |/ ___|___ | | | ___ ___| |_(_) ___ _ __ / _ \ _ __ _ __
# | | | |/ _ \| | | |/ _` | | / _ \| | |/ _ \/ __| __| |/ _ \| `_ \| | | | `_ \| `_ |
# | |___| | (_) | |_| | (_| | |__| (_) | | | __/ (__| |_| | (_) | | | | |_| | |_) | |_) |
# \____|_|\___/ \__,_|\__,_|\____\___/|_|_|\___|\___|\__|_|\___/|_| |_|\___/| .__/| .__/
# |_| |_|
# Projet : CloudCollectionApp
# Date de creation : 2026-08-23
# Auteurs : OpenAI ChatGPT, Codex, Binda Sébastien
# Licence : Apache 2.0
#
# Description : exports du domaine des retours utilisateurs.

from .github_feedback_configuration import GitHubFeedbackConfiguration
from .github_feedback_service import GitHubFeedbackService

__all__ = ["GitHubFeedbackConfiguration", "GitHubFeedbackService"]
84 changes: 84 additions & 0 deletions backend/services/feedback/github_feedback_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ____ _ _ ____ _ _ _ _ ___
# / ___| | ___ _ _ __| |/ ___|___ | | | ___ ___| |_(_) ___ _ __ / _ \ _ __ _ __
# | | | |/ _ \| | | |/ _` | | / _ \| | |/ _ \/ __| __| |/ _ \| `_ \| | | | `_ \| `_ |
# | |___| | (_) | |_| | (_| | |__| (_) | | | __/ (__| |_| | (_) | | | | |_| | |_) | |_) |
# \____|_|\___/ \__,_|\__,_|\____\___/|_|_|\___|\___|\__|_|\___/|_| |_|\___/| .__/| .__/
# |_| |_|
# Projet : CloudCollectionApp
# Date de creation : 2026-08-23
# Auteurs : OpenAI ChatGPT, Codex, Binda Sébastien
# Licence : Apache 2.0
#
# Description : configuration GitHub utilisee pour les retours beta.

from dataclasses import dataclass
import os

from services.security import EnvironmentSecretReader


@dataclass(frozen=True)
class GitHubFeedbackConfiguration:
"""Decrit la configuration de creation d'issues GitHub pour les retours beta."""

repository: str
token: str
labels: tuple[str, ...]
title_prefix: str

DEFAULT_LABELS = ("feedback", "remarque")
DEFAULT_TITLE_PREFIX = "[Retour utilisateur]"

@classmethod
def from_environment(cls) -> "GitHubFeedbackConfiguration":
"""Construit la configuration depuis les variables d'environnement.

Args:
Aucun.

Returns:
GitHubFeedbackConfiguration: Configuration GitHub lue et nettoyee.

Raises:
ValueError: Si une valeur configuree est invalide.
"""

labels = cls._parse_labels(os.getenv("GITHUB_FEEDBACK_LABELS", "feedback,remarque"))
configuration = cls(
repository=(os.getenv("GITHUB_FEEDBACK_REPOSITORY") or "").strip(),
token=(EnvironmentSecretReader.read("GITHUB_FEEDBACK_TOKEN") or "").strip(),
labels=labels,
title_prefix=(
os.getenv("GITHUB_FEEDBACK_TITLE_PREFIX", cls.DEFAULT_TITLE_PREFIX).strip()
or cls.DEFAULT_TITLE_PREFIX
),
)
configuration.validate()
return configuration

def validate(self) -> None:
"""Valide la coherence de la configuration GitHub.

Args:
Aucun.

Returns:
None: La methode ne retourne aucune valeur.

Raises:
ValueError: Si le depot ou le token est invalide.
"""

if "/" not in self.repository or len(self.repository.split("/")) != 2:
raise ValueError("GITHUB_FEEDBACK_REPOSITORY doit utiliser le format owner/repository.")
if not self.token:
raise ValueError("GITHUB_FEEDBACK_TOKEN est requis pour creer une issue GitHub.")

@classmethod
def _parse_labels(cls, raw_labels: str) -> tuple[str, ...]:
labels = tuple(
label.strip()
for label in str(raw_labels or "").split(",")
if label.strip()
)
return labels or cls.DEFAULT_LABELS
Loading
Loading