Files
GGThedandClaude Opus 5 aebc28fb8a docs: une seule source d architecture, et un README qui dit vrai
DOC-002.

docs/architecture.html (746 l.) et docs/architecture-v3.html (798 l.)
coexistaient, en HTML versionne, sans rien qui indique laquelle faisait foi
ni ou etait passee la v2. docs/architecture.md reprend la v3 -- la plus
complete : 24 classes au lieu de 19, plus la structure des paquets -- en
Markdown avec les diagrammes en Mermaid. Lisible en revue, comparable en
diff, rendu directement par Gitea comme par GitHub.

Ajoute au document ce que les diagrammes ne montrent pas et qu il faut
savoir avant de les lire : la double modelisation coach/equipe, l absence
de migrations, et le bot dans le meme processus que le web.

README
Il annoncait « Authorization Checks: Proper ownership validation on all
sensitive operations » a une epoque ou trois IDOR etaient ouverts, et
« Rate Limiting » sans mentionner que trusted_proxy='*' la rend
contournable. Il ne disait pas non plus comment installer, lancer ni
tester le projet.

Reecrit en francais, avec deux sections distinctes : ce qui est en place et
verifie par des tests, et **ce qui ne l est pas** -- migrations absentes,
secrets non revoques, proxy de confiance, identite Discord non prouvee.
Une liste de securite qui ne mentionne que les bonnes nouvelles est pire
qu absente : on s y fie.

Instructions d installation, de lancement et de verification ajoutees, avec
les deux seuls points d entree (ARCH-007).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-08 18:25:20 -04:00

16 KiB

Architecture

Source unique. docs/architecture.html et docs/architecture-v3.html coexistaient, en HTML versionné, sans rien qui indique laquelle faisait foi ni où était passée la v2 (DOC-002). Ce fichier reprend le contenu de la v3, la plus complète des deux, en Markdown : lisible en revue de code, comparable en diff, et rendu directement par Gitea comme par GitHub.

Les diagrammes sont en Mermaid. Ils décrivent la structure, qui bouge lentement ; les chiffres qui bougent vite (nombre de routes, de tests) sont tenus à jour dans le README, pas ici.


1. Vue d'ensemble

Couche Contenu
Entrée wsgi.py (Waitress, production) · run.py (développement)
Fabrique app/app.pycreate_app(config=None), blueprints, CSP, journalisation
Routes 7 blueprints. users est un paquet de six modules, un seul blueprint
Services app/services/ — notifications Discord, annonce des matchs planifiés
Autorisation app/permissions.py — point de vérité unique (ARCH-002)
Modèles app/models/ — héritage polymorphe à table unique sur User
Gabarits Jinja2, rendu serveur, nonce CSP sur chaque bloc <script>

À savoir avant de lire le reste. Un coach est rattaché à une équipe de deux façons — la colonne héritée OrgTeam.coach_id et la relation many-to-many OrgTeam.coaches — et les deux sont peuplées. Le diagramme de classes montre les deux. Ne jamais interroger l'une sans l'autre : passer par app/permissions.py. La fusion des deux est ARCH-001, qui attend une migration de données.


2. Diagramme de classes

classDiagram
    class BaseAvailability {
        +int day_of_week
        +time start_time
        +time end_time
        +datetime created_at
        +datetime updated_at
    }
    class PlayerDisponibility {
        +int id
        +int player_id
    }
    class CoachAvailability {
        +int id
        +int coach_id
    }
    BaseAvailability <|-- PlayerDisponibility : extends
    BaseAvailability <|-- CoachAvailability : extends

    class BaseMatch {
        +str title
        +str description
        +date date
        +time start_time
        +time end_time
        +str location
        +str status
        +int created_by
        +datetime created_at
    }
    class Match {
        +int id
        +int tryout_id
        +str match_type
        +int team1_id
        +int team2_id
        +get_participating_players()
    }
    class TeamMatch {
        +int id
        +int org_team_id
        +str opponent
        +get_confirmed_count()
    }
    BaseMatch <|-- Match : extends
    BaseMatch <|-- TeamMatch : extends

    class BaseParticipant {
        +int player_id
        +datetime added_at
    }
    class MatchParticipant {
        +int id
        +int match_id
        +int team_side
        +str position
        +bool attendance_confirmed
    }
    class TeamMatchParticipant {
        +int id
        +int team_match_id
        +bool is_confirmed
    }
    BaseParticipant <|-- MatchParticipant : extends
    BaseParticipant <|-- TeamMatchParticipant : extends

    class User {
        +int id
        +str username
        +str password_hash
        +str role
        +str full_name
        +str email
        +str phone
        +bool is_active_account
        +datetime created_at
        +int failed_login_attempts
        +datetime locked_until
        +str games
        +str discord_username
        +str discord_user_id
        +str league_os_profile
        +get_games_list()
        +get_gamertags()
        +get_org_teams()
        +can_evaluate()
        +can_manage_users()
        +can_manage_teams()
        +can_manage_tryouts()
        +can_schedule_matches()
        +can_manage_this_tryout()
        +can_manage_this_org_team()
        +get_visible_tryouts()
    }
    class Admin {
        +all permissions = True
    }
    class Manager {
        +can_manage_teams()
        +can_manage_tryouts()
    }
    class Coach {
        +can_evaluate()
        +can_schedule_matches()
        +can_manage_this_tryout()
        +can_manage_this_org_team()
    }
    class Player {
        +get_visible_tryouts()
    }
    class Scout {
        +can_evaluate()
        +get_visible_tryouts()
    }
    User <|-- Admin : polymorphic
    User <|-- Manager : polymorphic
    User <|-- Coach : polymorphic
    User <|-- Player : polymorphic
    User <|-- Scout : polymorphic

    class UserGamertag {
        +int id
        +int user_id
        +str game
        +str gamertag
        +str platform
        +get_trn_url()
    }
    class OrgTeam {
        +int id
        +str name
        +int created_by
        +datetime created_at
        +int coach_id
        +int manager_id
        +get_coaches()
        +get_managers()
        +players()
        +get_players_with_status()
    }
    class TeamPlayer {
        +int id
        +int player_id
        +int org_team_id
        +str status
        +str position
        +datetime added_at
    }
    class Tryout {
        +int id
        +str title
        +str description
        +str game
        +date date
        +str location
        +str status
        +int max_players
        +int created_by
        +int target_org_team_id
        +int manager_id
        +int coach_id
        +datetime created_at
    }
    class TryoutRegistration {
        +int id
        +int tryout_id
        +int player_id
        +datetime registered_at
        +str status
        +str notes
    }
    class Evaluation {
        +int id
        +int tryout_id
        +int player_id
        +int evaluator_id
        +int mecanics_score
        +int cohesion_score
        +int communication_score
        +int gamesense_score
        +int versatility_score
        +int discipline_score
        +int analysis_score
        +int sport_ethics_score
        +int mental_score
        +float overall_score
        +str comments
        +str position_recommendation
        +datetime created_at
        +datetime updated_at
    }
    class Team {
        +int id
        +int tryout_id
        +str name
        +int created_by
        +datetime created_at
    }
    class TeamMember {
        +int id
        +int team_id
        +int player_id
        +str position
        +datetime added_at
    }
    class Contract {
        +int id
        +int player_id
        +int team_id
        +int uploaded_by_id
        +str original_filename
        +str stored_filename
        +str file_path
        +str signed_filename
        +str signed_file_path
        +str status
        +str notes
        +datetime uploaded_at
        +datetime signed_at
        +can_view()
        +can_upload_signed()
    }
    class TeamNote {
        +int id
        +int org_team_id
        +int coach_id
        +str content
        +datetime created_at
        +datetime updated_at
    }
    class PersonalNote {
        +int id
        +int player_id
        +int coach_id
        +str content
        +datetime created_at
        +datetime updated_at
        +int match_id
        +int team_id
        +int tryout_id
    }
    class OneOnOneRequest {
        +int id
        +int player_id
        +int coach_id
        +int org_team_id
        +date date
        +time start_time
        +time end_time
        +str points
        +str status
        +datetime created_at
        +datetime responded_at
        +bigint discord_message_id
        +str coach_rejection_message
    }
    class load_user {
        +load_user(user_id)
    }

    User "1" --> "*" UserGamertag : gamertags
    User "1" --> "*" Tryout : created_tryouts
    User "1" --> "*" Tryout : managed_tryouts
    User "1" --> "*" Tryout : coached_tryouts
    User "1" --> "*" Evaluation : evaluations_given
    User "1" --> "*" Evaluation : evaluations_received
    User "1" --> "*" TryoutRegistration : registrations
    User "1" --> "*" TeamMember : team_assignments
    User "1" --> "*" TeamPlayer : team_placements
    User "1" --> "*" Match : created_matches
    User "1" --> "*" TeamMatch : created_team_matches
    User "1" --> "*" PlayerDisponibility : disponibilities
    User "1" --> "*" CoachAvailability : availabilities
    User "1" --> "*" Team : created_teams
    User "1" --> "*" Contract : contracts
    User "1" --> "*" PersonalNote : personal_notes
    User "1" --> "*" OneOnOneRequest : one_on_one_requests

    OrgTeam "1" --> "*" TeamPlayer : team_players
    OrgTeam "1" --> "*" Tryout : tryouts
    OrgTeam "1" --> "*" TeamNote : team_notes
    OrgTeam "1" --> "*" TeamMatch : team_matches
    OrgTeam "1" --> "*" OneOnOneRequest : requests
    OrgTeam "1" --> "*" Contract : contracts

    Tryout "1" --> "*" TryoutRegistration : registrations
    Tryout "1" --> "*" Evaluation : evaluations
    Tryout "1" --> "*" Team : teams
    Tryout "1" --> "*" Match : matches
    Tryout "1" --> "*" PersonalNote : notes

    Team "1" --> "*" TeamMember : members
    Team "1" --> "*" Match : as_team1
    Team "1" --> "*" Match : as_team2
    Match "1" --> "*" MatchParticipant : participants
    Match "1" --> "*" PersonalNote : notes

    TeamMatch "1" --> "*" TeamMatchParticipant : participants

    User .. load_user : loads

3. Structure du paquet app/models

graph TD
    subgraph MODELS["app/models/ — 31 files"]
        direction TB
        INIT["__init__.py — master re-exporter"]
        CONST["_constants.py — USER_TYPES, ESPORT_GAMES, GAME_POSITIONS, etc."]
        LOADERS["_loaders.py — Flask-Login load_user()"]
        ASSOC["_associations.py — M2M association tables"]

        subgraph USER_PKG["user_model/ (7 files)"]
            USER["user.py — User base (polymorphic)"]
            ADMIN["admin.py — Admin(User)"]
            MGR["manager.py — Manager(User)"]
            COACH["coach.py — Coach(User)"]
            PLAYER["player.py — Player(User)"]
            SCOUT["scout.py — Scout(User)"]
        end

        subgraph AVAIL_PKG["availability/ (4 files)"]
            BASE_AVAIL["base.py — BaseAvailability (abstract)"]
            PD["player_disponibility.py — PlayerDisponibility"]
            CA["coach_availability.py — CoachAvailability"]
        end

        subgraph MATCH_PKG["match_model/ (4 files)"]
            BASE_M["base.py — BaseMatch (abstract)"]
            MATCH["match.py — Match (tryout-scoped)"]
            TM["team_match.py — TeamMatch (regular season)"]
        end

        subgraph PARTIC_PKG["participant/ (4 files)"]
            BASE_P["base.py — BaseParticipant (abstract)"]
            MP["match_participant.py — MatchParticipant"]
            TMP["team_match_participant.py — TeamMatchParticipant"]
        end

        subgraph ORG_PKG["org_team/ (3 files)"]
            ORG["org_team.py — OrgTeam"]
            TP["team_player.py — TeamPlayer"]
        end

        subgraph TRYOUT_PKG["tryout/ (3 files)"]
            TRY["tryout.py — Tryout"]
            TR["tryout_registration.py — TryoutRegistration"]
        end

        subgraph TEAM_PKG["team/ (3 files)"]
            TEAM["team.py — Team (tryout-specific)"]
            TMEMBER["team_member.py — TeamMember"]
        end

        subgraph STANDALONE["6 Standalone Files"]
            EVAL["evaluation.py"]
            CONTRACT["contract.py"]
            GT["user_gamertag.py"]
            TN["team_note.py"]
            PN["personal_note.py"]
            OOO["one_on_one_request.py"]
        end
    end

    INIT --> USER_PKG
    INIT --> AVAIL_PKG
    INIT --> MATCH_PKG
    INIT --> PARTIC_PKG
    INIT --> ORG_PKG
    INIT --> TRYOUT_PKG
    INIT --> TEAM_PKG
    INIT --> STANDALONE
    INIT --> CONST
    INIT --> LOADERS
    INIT --> ASSOC

4. Flux d'une requête

graph TD
    subgraph CLIENT["Client Layer"]
        BROWSER["Browser / User"]
        DISCORD_APP["Discord App"]
        MONITOR["Monitoring / LB"]
    end

    subgraph PROXY["Reverse Proxy"]
        NGINX["Nginx<br/>TLS termination<br/>Static files<br/>Rate limiting"]
    end

    subgraph APP["Flask Application — create_app()"]
        subgraph INGRESS["Incoming Middleware"]
            BEFORE_REQ["before_request<br/>force_https()"]
            CSRF_CHECK["CSRF Validation"]
            LIMITER_CHECK["Rate Limiter<br/>200/day | 50/hr"]
            LOGIN_CHECK["login_required<br/>Permission checks"]
        end

        subgraph BLUEPRINTS["Route Blueprints (8)"]
            AUTH_BP["auth_bp /auth<br/>login, register, logout"]
            MAIN_BP["main_bp /<br/>dashboard, privacy, terms"]
            TRYOUTS_BP["tryouts_bp /tryouts<br/>CRUD + register"]
            TEAMS_BP["teams_bp /teams<br/>OrgTeam CRUD"]
            MATCHES_BP["matches_bp /matches<br/>Tryout match CRUD"]
            TEAM_MATCHES_BP["team_matches_bp /team-matches<br/>Season match CRUD"]
            USERS_BP["users_bp /users<br/>User CRUD, contracts<br/>disponibilities, one-on-one"]
            EVALS_BP["evaluations_bp /evaluations<br/>9-score evaluations"]
        end

        subgraph EGRESS["Outgoing Middleware"]
            AFTER_REQ["after_request<br/>Security headers"]
            ERRORS["Error Handlers<br/>400 401 403 404 429 500"]
            HEALTH["GET /health"]
        end
    end

    subgraph EXTENSIONS["Flask Extensions"]
        DB_EXT["SQLAlchemy db"]
        LOGIN_EXT["LoginManager"]
        CSRF_EXT["CSRFProtect"]
        LIMITER_EXT["Flask-Limiter"]
    end

    subgraph MODELS["SQLAlchemy Models (7 subpackages)"]
        USER_M["user_model/ — User hierarchy<br/>User, Admin, Manager, Coach, Player, Scout"]
        AVAIL_M["availability/ — BaseAvailability<br/>PlayerDisponibility, CoachAvailability"]
        MATCH_M["match_model/ — BaseMatch<br/>Match, TeamMatch"]
        PARTIC_M["participant/ — BaseParticipant<br/>MatchParticipant, TeamMatchParticipant"]
        ORG_M["org_team/ — OrgTeam, TeamPlayer"]
        TRYOUT_M["tryout/ — Tryout, TryoutRegistration"]
        TEAM_M["team/ — Team, TeamMember"]
        STANDALONE_M["Standalone: Evaluation, Contract<br/>UserGamertag, TeamNote, PersonalNote<br/>OneOnOneRequest"]
    end

    subgraph DB["Database"]
        SQLITE["SQLite / PostgreSQL<br/>DATABASE_URL"]
    end

    subgraph UTILS["Utility Modules"]
        VALIDATORS["validators.py<br/>Input validation schemas"]
        LOGGING["logging_config.py<br/>Structured logging"]
        DISCORD_BOT["discord_bot.py<br/>Discord notifications<br/>Reaction handling"]
        SECURITY["security_scan.py<br/>Security audit"]
        SEED["seed.py<br/>Database seeding"]
        BACKUP["backup.py<br/>Database backup"]
    end

    subgraph TEMPLATES["Jinja2 Templates"]
        PAGES["templates/pages/<br/>~30 HTML pages"]
        LAYOUTS["templates/layouts/<br/>base, nav"]
        ERRORS_TPL["templates/errors/<br/>400-500 errors"]
    end

    subgraph STATIC["Static Assets"]
        CSS_F["static/css/"]
        JS_F["static/js/"]
    end

    subgraph EXTERNAL["External APIs"]
        TRN_API["TRN / Tracker.gg<br/>Gamertag profiles"]
        DISCORD_API["Discord API<br/>Bot notifications"]
    end

    BROWSER --> NGINX
    NGINX --> BEFORE_REQ
    BEFORE_REQ --> CSRF_CHECK
    CSRF_CHECK --> LIMITER_CHECK
    LIMITER_CHECK --> LOGIN_CHECK
    LOGIN_CHECK --> BLUEPRINTS
    BLUEPRINTS --> AFTER_REQ
    AFTER_REQ --> BROWSER

    AUTH_BP -.-> USER_M
    TRYOUTS_BP -.-> TRYOUT_M
    EVALS_BP -.-> STANDALONE_M
    TEAMS_BP -.-> ORG_M
    MATCHES_BP -.-> MATCH_M
    TEAM_MATCHES_BP -.-> MATCH_M
    USERS_BP -.-> USER_M

    MODELS --> DB

    DB_EXT --> MODELS
    LOGIN_EXT --> USER_M
    CSRF_EXT --> BLUEPRINTS
    LIMITER_EXT --> BLUEPRINTS

    VALIDATORS -.-> AUTH_BP
    VALIDATORS -.-> USERS_BP
    LOGGING -.-> APP
    DISCORD_BOT --> DISCORD_API
    DISCORD_BOT -.-> MATCHES_BP
    DISCORD_BOT -.-> TEAM_MATCHES_BP
    DISCORD_BOT -.-> USERS_BP
    SEED -.-> DB
    BACKUP -.-> DB

    BLUEPRINTS --> TEMPLATES
    TEMPLATES --> STATIC

    USER_M -.-> TRN_API
    MONITOR --> HEALTH
    DISCORD_APP --> DISCORD_BOT

5. Ce que ces diagrammes ne disent pas

  • Les migrations. Il n'y en a pas. Le schéma est créé par db.create_all(), qui crée les tables absentes et n'ALTER jamais : une colonne ajoutée à un modèle n'existe pas en production. C'est DB-002/DB-004, et c'est ce qui bloque la moitié du reste.
  • Le bot Discord tourne dans un fil démon du même processus que le serveur web. Son état est exposé par /health.
  • Les autorisations réelles. Le diagramme montre les classes, pas les règles. Celles-ci sont dans app/permissions.py et dans les méthodes can_* des sous-classes de User.