diff --git a/README.md b/README.md index 91c27d9..f49ee07 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,133 @@ # Plateforme centralisée de tryouts -## Security Configuration +Application interne du club e-sport de l'UdeS : inscriptions aux sélections, +évaluations, gestion des équipes, disponibilités, contrats, et notifications +Discord. -### Required Environment Variables +Le site est servi **en français**, l'anglais reste accessible par le sélecteur +de la barre latérale (voir `docs/translations.md`). -Before deploying, create a `.env` file which integrates everything in the .env.exemple. -Ensure you follow the comments of the exemple if you are to use this tool in production. +--- +## Démarrer -### Security Features Implemented +```bash +python -m venv .venv +.venv/Scripts/pip install -r requirements.txt -r requirements-dev.txt +cp app/.env.exemple .env # puis remplir les valeurs +.venv/Scripts/python run.py # développement, http://127.0.0.1:5000 +``` -- **Rate Limiting**: Login endpoint limited to 10 requests per minute to prevent brute-force attacks -- **Secure Session Cookies**: HTTPSOnly, SameSite=Lax, and Secure flags enabled -- **CSRF Protection**: Enabled by default on all forms -- **HTTPS Enforcement**: Automatic redirect to HTTPS in production -- **Security Headers**: X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, HSTS -- **Open Redirect Prevention**: URL validation on login redirect -- **Authorization Checks**: Proper ownership validation on all sensitive operations -- **nginx**: reverse-proxy and load balancer -- **Waitress WSGI**: Production ready WSGI +`SECRET_KEY` et `DATABASE_URL` sont **obligatoires** : `create_app()` refuse +de démarrer sans eux. `DATABASE_URL` doit pointer sur PostgreSQL ; le pilote +psycopg 3 est nommé automatiquement si l'URL n'en nomme pas. -### When true in .env: -- **Forces HTTPS only** -- **Forcer secure cookies** +Production : `python wsgi.py` (Waitress derrière nginx). Voir +`docs/deployment.md`. -## App details +Ce sont les **deux seuls** points d'entrée. -### Code +## Vérifier -- Full python backend using flask -- statics are pure HTML and CSS -- Some js to add logic to styling and showing certain pages/cards +```bash +.venv/Scripts/python -m pytest # suite complète +.venv/Scripts/python -m ruff check . # lint +.venv/Scripts/python -m ruff format --check . +``` -### Functionalities +Les trois tournent en CI et y sont bloquants. -- **User base with sign-ins**: Forces users to create an account and register pertinent information for tryouts and teams. The admin can attribute them a role. -- **User-Role-Based Permissions**: admin - full acces, coach/manager - access to team management, player - views what he is registered in (no management), scout - view only -- **Tryout Management**: manage internal tryout teams, organise internal tryouts matches (3 formats, team vs team, PvP, scrim). Coaches can Evaluate players based on 10 criteria -- **Team Management**: manage teams for the season, create matches and practices. When planning a practice there will be a calendar showing player availabitlities slots to help chose a time -- **Coach and Player Availabilities**: Allow better planning for the coaches, and for players to book One on Ones with their coach. -- **Player Notes**: Coaches can give notes to their players. The players will see them and there is a history which keeps the most recent notes. -- **Team Notes**: Coaches can give notes to their teams, where all players from that team can see the note. -- **One on One**: Players can request a One on One meeting with their coach. This sends a discord dm to the coach to accept or refuse. The player is then notified of the response. -- **Availabilities**: Allow players and coach to enter the moments they are available. Allows for easier practice setup and One on One planning. +--- +## Ce que fait l'application -## Discord Integration +- **Comptes et rôles** — cinq rôles : président (`admin`), gérant + (`manager`), coach, joueur (`player`), recruteur (`scout`). Le président + attribue les rôles. +- **Sélections** — organisation des tryouts, trois formats de match + (équipe contre équipe, joueur contre joueur, scrim), évaluation des + joueurs sur dix critères. +- **Équipes** — effectifs de la saison, matchs et entraînements. Le + formulaire d'entraînement affiche les disponibilités des joueurs. +- **Disponibilités** — créneaux hebdomadaires des joueurs, créneaux + réservables des coachs. +- **Notes** — un coach écrit des notes d'équipe (visibles par l'équipe) et + des notes nominatives (visibles par le joueur concerné). +- **Un-à-un** — un joueur demande une séance à son coach ; le coach répond + depuis le site ou par une réaction sur le message privé Discord. +- **Contrats** — dépôt d'un contrat par le staff, signature par le joueur. -The application supports sending Discord direct messages to coaches when players request One on One sessions, -when matches/tryouts/practices are created and a player is in it, and the players get match reminders 24h before a match. +## Comment c'est construit -When sending a **One on One** request, the coach can accept via the platform or react to the discord message to answer the booking request. -Same thing with **matches** and **practices**, the players can react or answer on the platform. +Backend Python 3.12 / Flask, rendu serveur en Jinja2, CSS et JavaScript +maison, sans framework front. Base PostgreSQL via SQLAlchemy. Bot Discord +(`discord.py`) dans un fil du même processus que le serveur web. -### Setup Instructions +`docs/architecture.md` contient les diagrammes (classes, paquets, flux +d'une requête). -#### 1. Create a Discord Bot (Not needed for UdeS user, the bot already exists) +--- -1. Go to the [Discord Developer Portal](https://discord.com/developers/applications) -2. Create a new application -3. Go to the "Bot" tab and create a bot user -4. Copy the bot token - this will be your `DISCORD_BOT_TOKEN` -5. Enable the "Message Content Intent" under Privileged Gateway Intents (required for sending messages) +## Sécurité -#### 2. Add Bot to your server +En place et vérifié par des tests : -For the bot to send DMs: -1. Each user must have the bot added to their Discord server OR be friends with the bot -2. Users need to add their Discord User ID to their profile: - - Enable Developer Mode in Discord (User Settings → Advanced → Developer Mode) - - Right-click on their profile → Copy ID - - Enter this numeric ID in the "Discord User ID" field in their profile settings +- **Limitation de débit** sur la connexion (10 requêtes/minute par IP). +- **Cookies de session** `HttpOnly`, `SameSite=Lax`, `Secure`, avec + expiration effective. +- **CSRF** sur tous les formulaires, y compris la déconnexion (en POST). +- **HTTPS** forcé en production, **HSTS**. +- **CSP sans `unsafe-inline`** sur `script-src` : aucun gestionnaire + d'événement en ligne, chaque bloc ` - - - - -
-

🏆 Team Tryouts — Architecture Documentation

-

UML Class Diagram & Functional System Architecture

- -
- -
- - -
-

📊 Project Statistics

-
-
-
19
-
SQLAlchemy Model Classes
-
-
-
8
-
Route Blueprints
-
-
-
4
-
Flask Extensions
-
-
-
2
-
Association Tables (M2M)
-
-
-
50+
-
Route Endpoints
-
-
-
8
-
Utility Modules
-
-
-
- - -
-

📐 UML Class Diagram — All 19 Model Classes

-
- -
-classDiagram - 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() - +has_role() - +can_evaluate() - +can_manage_users() - +can_manage_tryouts() - +can_manage_teams() - +can_schedule_matches() - +can_manage_this_tryout() - +can_manage_this_org_team() - +get_gamertags() - +get_org_teams() - } - 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 Match { - +int id - +int tryout_id - +str title - +str description - +date date - +time start_time - +time end_time - +str location - +str status - +str match_type - +int created_by - +datetime created_at - +int team1_id - +int team2_id - +get_participating_players() - } - class PlayerDisponibility { - +int id - +int player_id - +int day_of_week - +time start_time - +time end_time - +datetime created_at - +datetime updated_at - } - class MatchParticipant { - +int id - +int match_id - +int player_id - +int team_side - +str position - +bool attendance_confirmed - +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 CoachAvailability { - +int id - +int coach_id - +int day_of_week - +time start_time - +time end_time - +datetime created_at - +datetime updated_at - } - 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 TeamMatch { - +int id - +int org_team_id - +str title - +str description - +str opponent - +date date - +time start_time - +time end_time - +str location - +str status - +int created_by - +datetime created_at - +get_confirmed_count() - } - class TeamMatchParticipant { - +int id - +int team_match_id - +int player_id - +bool is_confirmed - +datetime added_at - } - class load_user { - +load_user(user_id) - } - - User "1" --> "*" UserGamertag : gamertags - User "1" --> "*" Tryout : created_by - User "1" --> "*" Tryout : managed_tryouts - User "1" --> "*" Tryout : coached_tryouts - User "1" --> "*" Evaluation : evaluations_given - User "1" --> "*" Evaluation : evaluations_received - User "1" --> "*" TryoutRegistration : tryout_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 : coach_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 : matches_as_team1 - Team "1" --> "*" Match : matches_as_team2 - Match "1" --> "*" MatchParticipant : participants - Match "1" --> "*" PersonalNote : notes - - TeamMatch "1" --> "*" TeamMatchParticipant : participants - - User .. load_user : loads -
- -
-
- - -
-

🔀 Functional Architecture — Request Flow & Component Graph

-
- -
-graph TD - subgraph CLIENT["🌐 Client Layer"] - BROWSER["Browser / User"] - DISCORD_APP["Discord App"] - MONITOR["Monitoring / LB"] - end - - subgraph PROXY["🔄 Reverse Proxy"] - NGINX["Nginx
(TLS termination,
static files,
rate limiting)"] - end - - subgraph APP["🐍 Flask Application (create_app)"] - direction TB - - subgraph INGRESS["Incoming"] - BEFORE_REQ["@before_request
force_https()"] - CSRF_CHECK["CSRF Validate"] - LIMITER_CHECK["Rate Limiter
200/day · 50/hr"] - LOGIN_CHECK["@login_required"] - end - - subgraph BLUEPRINTS["📦 Route Blueprints (8 total)"] - AUTH_BP["auth_bp
━━━━━━━
⁕ /auth/login
⁕ /auth/register
⁕ /auth/logout
⁕ /auth/profile
⁕ /auth/change-password
⁕ /auth/delete-account"] - MAIN_BP["main_bp
━━━━━━━
⁕ /
⁕ /dashboard
⁕ /privacy
⁕ /terms"] - TRYOUTS_BP["tryouts_bp
━━━━━━━
⁕ /tryouts/
⁕ /tryouts/create
⁕ /tryouts/{id}
⁕ /tryouts/{id}/register
⁕ /tryouts/{id}/edit
⁕ /tryouts/{id}/delete"] - TEAMS_BP["teams_bp
━━━━━━━
⁕ /teams/
⁕ /teams/create
⁕ /teams/{id}
⁕ /teams/{id}/edit
⁕ /teams/{id}/delete
⁕ /teams/{id}/add-player"] - MATCHES_BP["matches_bp
━━━━━━━
⁕ /matches/
⁕ /matches/create
⁕ /matches/{id}
⁕ /matches/{id}/edit"] - TEAM_MATCHES_BP["team_matches_bp
━━━━━━━
⁕ /team-matches/
⁕ /team-matches/create
⁕ /team-matches/{id}
⁕ /team-matches/{id}/edit
⁕ /team-matches/{id}/delete"] - USERS_BP["users_bp
━━━━━━━
⁕ /users/
⁕ /users/create
⁕ /users/{id}
⁕ /users/{id}/delete
⁕ /users/disponibilities/
⁕ /users/coach-availability/
⁕ /users/api/gamertags"] - EVALS_BP["evaluations_bp
━━━━━━━
⁕ /evaluations/
⁕ /evaluations/create
⁕ /evaluations/{id}
⁕ /evaluations/{id}/edit
⁕ /evaluations/{id}/delete"] - end - - subgraph EGRESS["Outgoing"] - AFTER_REQ["@after_request
add_security_headers()"] - ERROR_HANDLERS["Error Handlers
400 · 401 · 403 · 404
429 · 500 · HTTPException"] - HEALTH["GET /health"] - end - end - - subgraph EXTENSIONS["🔌 Flask Extensions"] - DB_EXT["SQLAlchemy
(db)"] - LOGIN_EXT["Flask-Login
(login_manager)"] - CSRF_EXT["CSRFProtect
(csrf)"] - LIMITER_EXT["Flask-Limiter
(limiter)"] - end - - subgraph MODELS["🗄️ SQLAlchemy Models (models.py)"] - direction LR - USER_M["User"] - TRYOUT_M["Tryout"] - EVAL_M["Evaluation"] - ORGTEAM_M["OrgTeam"] - TEAM_M["Team"] - MATCH_M["Match"] - TEAMMATCH_M["TeamMatch"] - OTHERS_M["...15 more classes"] - end - - subgraph DB["💾 Database"] - SQLITE["SQLite / PostgreSQL
(SQLALCHEMY_DATABASE_URI)"] - end - - subgraph UTILS["🛠️ Utility Modules"] - VALIDATORS["validators.py
Password/username validation"] - LOGGING["logging_config.py
Structured logging"] - DISCORD_BOT["discord_bot.py
Discord notifications
+ reaction handling"] - SECURITY["security_scan.py
Security audit tool"] - SEED["seed.py
Database seeding"] - BACKUP["backup.py
Database backup"] - MIGRATE["migrate_usernames.py
Username migration"] - end - - subgraph TEMPLATES["🖼️ Jinja2 Templates"] - PAGES["templates/pages/
(~30 HTML pages)"] - LAYOUTS["templates/layouts/
(base, nav)"] - ERRORS_TPL["templates/errors/
(400, 401, 403, 404, 429, 500)"] - end - - subgraph STATIC["📁 Static Assets"] - CSS_F["static/css/"] - JS_F["static/js/"] - end - - subgraph EXTERNAL["🌍 External APIs"] - TRN_API["TRN / Tracker.gg
(gamertag profiles)"] - DISCORD_API["Discord API
(bot notifications)"] - end - - %% ========== FLOW ========== - 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 - - %% Blueprint to models - AUTH_BP -.-> USER_M - TRYOUTS_BP -.-> TRYOUT_M - EVALS_BP -.-> EVAL_M - TEAMS_BP -.-> ORGTEAM_M - MATCHES_BP -.-> MATCH_M - TEAM_MATCHES_BP -.-> TEAMMATCH_M - USERS_BP -.-> USER_M - - %% Models to DB - MODELS --> DB - - %% Extensions - DB_EXT --> MODELS - LOGIN_EXT --> USER_M - CSRF_EXT --> BLUEPRINTS - LIMITER_EXT --> BLUEPRINTS - - %% Utilities - 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 - - %% Templates & Static - BLUEPRINTS --> TEMPLATES - TEMPLATES --> STATIC - - %% External - USER_M -.-> TRN_API - MONITOR --> HEALTH - - %% Discord - DISCORD_APP --> DISCORD_BOT -
- -
-
- - -
-

🗺️ Legend

-
-
- - SQLAlchemy Models — Database entity classes -
-
- - Route Blueprints — Flask route groups -
-
- - Flask Extensions — SQLAlchemy, Login, CSRF, Limiter -
-
- - Utility Modules — Logging, validators, seeding, backups -
-
- - External APIs — Discord, TRN/Tracker.gg -
-
- - Middleware — Security headers, HTTPS redirect, error handlers -
-
- ─── Solid arrow - = HTTP request flow -
-
- - - - Dashed arrow - = Data access / logical dependency -
-
-
- -
- - - - - - - \ No newline at end of file diff --git a/docs/architecture-v3.html b/docs/architecture.md similarity index 59% rename from docs/architecture-v3.html rename to docs/architecture.md index 8a32621..35c080b 100644 --- a/docs/architecture-v3.html +++ b/docs/architecture.md @@ -1,199 +1,41 @@ - - - - - - Team Tryouts — Architecture v3 (Refactored) - - - - +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. -
-

🏆 Team Tryouts — Architecture v3 (Refactored)

-

UML Class Diagram & Functional Graph — 7 Subpackages + 6 Standalone Models

- -
+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. -
+--- - -
-

📊 Project Statistics

-
-
31
Python files in models/
-
7
Subpackages
-
24
Classes (incl. abstract)
-
8
Route Blueprints
-
4
Flask Extensions
-
8
Utility Modules
-
-
+## 1. Vue d'ensemble - -
-

📐 UML Class Diagram — All 24 Classes, Relationships & Inheritance

-
+| Couche | Contenu | +|---|---| +| Entrée | `wsgi.py` (Waitress, production) · `run.py` (développement) | +| Fabrique | `app/app.py` — `create_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 ` - - - \ No newline at end of file +- **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`.