Files
swift_companion/docs/pedagogie/guide-soutenance-realisation.md
Ladebeze66 a6ab13ee2f fix(profil): photo CDN avec Bearer OAuth et doc soutenance
Charge les avatars depuis cdn.intra.42.fr avec le même access token,
parse image.versions, et documente le flux pour la correction.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 10:38:16 +02:00

290 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Guide de correction / soutenance — ce qui a été réalisé
> **Objectif :** suivre le [sujet Swifty Companion v4](../sujets/swifty-companion.md) point par point, avec **fichiers**, **comportement** et **démo** à ne pas oublier en peer-eval.
> **Code évalué :** dépôt école = dossier [`swiftyCompanion/`](../../swiftyCompanion/) (pas `docs/`).
---
## 1. Vue d’ensemble
| Élément | Détail |
|---------|--------|
| Framework | **Flutter** (Dart), Material 3 |
| API | **42 Intra v2** — `https://api.intra.42.fr/v2` |
| Auth | **OAuth2** — `flutter_appauth` 12.x |
| Secrets | **`.env`** dans `swiftyCompanion/swifty_companion/.env` (asset, non versionné) |
| Vues | **2** — recherche (`SearchScreen`) + profil (`ProfileScreen`) |
| Bonus | **Refresh token** sur 401 + retry des requêtes API |
**Point d’entrée :** [`swiftyCompanion/swifty_companion/lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) → `home: SearchScreen()`.
---
## 2. Chapitre IV — Instructions générales & sécurité
| Exigence sujet | Réalisation | Fichiers |
|----------------|-------------|----------|
| Langage mobile (Flutter cité) | Projet Flutter standard | `swifty_companion/pubspec.yaml` |
| Dernière API 42 | Base URL v2 | [`lib/config/intra_config.dart`](../../swiftyCompanion/swifty_companion/lib/config/intra_config.dart) — `apiBaseUrl` |
| Secrets en **`.env`**, **pas dans Git** | `CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URI` | `.env` (local) · modèle [`swiftyCompanion/.env.example`](../../swiftyCompanion/.env.example) · ignore [`swiftyCompanion/.gitignore`](../../swiftyCompanion/.gitignore) et [`.gitignore` racine travail](../../.gitignore) |
| Chargement `.env` au démarrage | `dotenv.load` | [`lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) + asset dans `pubspec.yaml` (`assets: - .env`) |
**À dire en correction :** les clés OAuth ne sont jamais dans le code source ; l’évaluateur configure son propre `.env` à partir de `.env.example`.
---
## 3. Chapitre V.1 — Partie obligatoire (9 critères)
### Critère 1 — Au moins 2 vues
| Vue | Rôle | Fichier |
|-----|------|---------|
| **Vue 1** | Connexion API + recherche par login | [`lib/screens/search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) |
| **Vue 2** | Profil étudiant | [`lib/screens/profile_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart) |
**Démo :** montrer explicitement les deux écrans après une recherche réussie.
---
### Critère 2 — Gérer tous les cas d’erreur
| Situation | Message utilisateur (ex.) | Où c’est géré |
|-----------|---------------------------|---------------|
| Pas connecté OAuth | « Connecte-toi d’abord à l’API 42 » | [`intra_api_exception.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart) — `IntraNotAuthenticatedException` · [`intra_user_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart) |
| Login vide | « Saisis un login » | `IntraUserService.fetchUserByLogin` |
| Login inconnu | « Aucun utilisateur pour ce login » | `IntraUserNotFoundException` |
| Pas de réseau | « Connexion impossible. Vérifie le réseau. » | `IntraNetworkException` · `SocketException` / `ClientException` dans user + profile services |
| OAuth annulé / échec | Messages OAuth / plateforme | [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_connect()` |
| Session expirée (401) | « Session expirée. Reconnecte-toi… » | `IntraUnauthorizedException` · [`intra_http.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_http.dart) |
| Accès refusé (403) | Scopes `public` + `projects` | `IntraForbiddenException` |
| Trop de requêtes (429) | Message + attente / retry | `IntraRateLimitException` · retry dans `intra_http.dart` |
| Erreur profil (réseau, API) | Message + bouton **Réessayer** | `profile_screen.dart` — `_ErrorBody` |
| `.env` manquant | Texte explicite au lancement | `search_screen.dart` — branche `!IntraConfig.isConfigured` |
**Démo suggérée :** login `zzzzinvalid` → mode avion → (optionnel) profil en erreur + réessai.
---
### Critère 3 — Vue 2 : infos du login s’il existe
- Navigation **uniquement** si l’API renvoie un utilisateur : [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_search()` → `Navigator.push(ProfileScreen(user: …))`.
- Si liste vide → exception, **pas** d’ouverture du profil.
---
### Critère 4 — ≥ 4 détails + photo de profil
| Donnée affichée | Source API | Fichier UI |
|-----------------|------------|------------|
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
| **Photo** | `image.link`, `image.versions`, `image_url` | `_imageUrl` + `_IntraProfileAvatar` (`http.get` + Bearer via `cdnImageHeaders`) — `profile_screen.dart` |
| **Email** | `user['email']` | `_DetailsCard` |
| **Téléphone** | `user['phone']` | `_DetailsCard` |
| **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` |
| **Localisation** | `user['location']` | `_DetailsCard` |
| **Wallet** | `user['wallet']` | `_DetailsCard` |
Chargement profil : [`lib/services/intra_profile_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart) — `GET /users/{login}`, puis `projects_users`.
**Note sujet :** champs masqués par l’API → affichage `—` (confidentialité OAuth).
**Non affiché (optionnel sujet) :** `evaluation_points` — tu as déjà **> 4** champs sans lui.
---
### Critère 5 — Skills : niveau + pourcentage
- **Pas** `/experiences` (souvent 403).
- Skills depuis **`cursus_users[].skills`** sur la fiche user.
- Niveau entier = partie entière du `level` API ; **%** = partie décimale × 100.
- Parsing : `IntraProfileService._parseSkillsFromCursus` · affichage : `_SkillsCard` + barre de progression.
**Démo :** lire une ligne du type `Algorithms — niveau 8 (42 %)`.
---
### Critère 6 — Projets réalisés, y compris failed
- `GET /users/{id}/projects_users?page[size]=100`
- Filtre app : statuts **`finished`** et **`failed`** — `_parseProjects` dans `intra_profile_service.dart`.
- Liste : `_ProjectsCard` (libellés Validé / Échec + note si présente).
**Scope OAuth requis :** `projects` (sinon 403).
---
### Critère 7 — Revenir à la vue 1
- `MaterialPageRoute` + **retour système** / flèche AppBar sur le profil.
- `ProfileScreen` : `SwiftyUi.appBar(..., showBack: true)`.
- Session OAuth **conservée** sur `SearchScreen` (pas de nouveau login).
**Démo :** retour → nouvelle recherche sans « Se connecter avec 42 ».
---
### Critère 8 — Layout flexible / moderne
| Mécanisme | Fichier |
|-----------|---------|
| Scroll + clavier | `ResponsiveScrollBody` — [`lib/theme/swifty_ui.dart`](../../swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart) |
| Formulaire centré (vue 1) | `searchMaxWidth` 480 px |
| Profil 2 colonnes si large | `wideBreakpoint` 720 px — `profile_screen.dart` `LayoutBuilder` |
| Lignes détail empilées si étroit | `_DetailRow` si largeur < 400 px |
| Thème 42 turquoise | `SwiftyUi.brand` · `main.dart` `ColorScheme.fromSeed` |
**Démo :** rotation paysage ou grand écran → 2 colonnes sur le profil.
---
### Critère 9 — Un token OAuth, pas un token par requête
| Règle | Implémentation |
|-------|----------------|
| Un seul login OAuth | `IntraAuthService.instance` — [`intra_auth_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart) |
| Bearer sur chaque GET | `authHeaders()` |
| Pas de OAuth à chaque recherche | `IntraUserService` / `IntraProfileService` réutilisent le même service |
**À dire :** un flux `authorizeAndExchangeCode` au début ; ensuite uniquement des `GET` API avec le même access token.
---
## 4. OAuth2 Intra (encadrés sujet)
| Élément | Valeur / choix | Fichiers |
|---------|----------------|----------|
| Authorize | `https://api.intra.42.fr/oauth/authorize` | `intra_config.dart` |
| Token | `https://api.intra.42.fr/oauth/token` | `intra_config.dart` + refresh dans `intra_auth_service.dart` |
| Scopes | `public`, `projects` | `intra_auth_service.dart` — `AuthorizationTokenRequest` |
| Redirect (device) | `com.example.swiftycompanion://callback` | `.env` · [`intra-oauth-formulaire.md`](intra-oauth-formulaire.md) |
| Android redirect | `RedirectUriReceiverActivity` | [`android/app/src/main/AndroidManifest.xml`](../../swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml) |
| Gradle scheme | `appAuthRedirectScheme` | [`android/app/build.gradle.kts`](../../swiftyCompanion/swifty_companion/android/app/build.gradle.kts) |
**Doc perso (non rendue Intra) :** [etape-02-oauth-intra.md](etape-02-oauth-intra.md), [oauth-test-manuel.md](oauth-test-manuel.md).
---
## 5. Chapitre V.2 — Bonus (refresh token)
| Exigence | Réalisation | Fichiers |
|----------|-------------|----------|
| Stocker refresh au login | `result.refreshToken` | `intra_auth_service.dart` — `signIn()` |
| Rafraîchir à expiration | `POST` `grant_type=refresh_token` | `refreshAccessToken()` |
| App continue | Sur **401** → refresh → **rejouer** le GET | `intra_http.dart` — `intraApiGet(..., auth:)` |
| Parsing JSON token | | `intra_oauth_tokens.dart` + tests |
| Test manuel debug | Bouton « DEBUG : simuler token expiré » | `search_screen.dart` (`kDebugMode` uniquement) puis **Rechercher** |
**Limite assumée :** tokens en **mémoire** seulement — après kill de l’app, reconnexion OAuth (acceptable si tu l’expliques).
**Guide détaillé :** [etape-07-bonus-token.md](etape-07-bonus-token.md).
---
## 6. Inventaire des fichiers applicatifs (lib/)
Fichiers **métier** à connaître pour la soutenance :
```
swiftyCompanion/swifty_companion/lib/
├── main.dart # App + thème + dotenv
├── config/
│ └── intra_config.dart # URLs + variables .env
├── services/
│ ├── intra_auth_service.dart # OAuth, access + refresh
│ ├── intra_user_service.dart # Recherche login
│ ├── intra_profile_service.dart # Profil agrégé
│ ├── intra_http.dart # GET + 401/429
│ ├── intra_oauth_tokens.dart # Parse token JSON (bonus)
│ └── intra_api_exception.dart # Erreurs métier
├── screens/
│ ├── search_screen.dart # Vue 1
│ └── profile_screen.dart # Vue 2
└── theme/
└── swifty_ui.dart # Layout partagé (étape 5)
```
**Tests :**
```
test/
├── widget_test.dart # Démarre app + bouton connexion
└── intra_oauth_tokens_test.dart # Parse refresh (bonus)
```
**Fichier supprimé (nettoyage) :** `auth_screen.dart` (écran test obsolète, non utilisé).
**Plateforme Android (OAuth physique) :** `AndroidManifest.xml`, `build.gradle.kts` (voir §4).
---
## 7. Endpoints API utilisés
| Usage | Méthode | Appelé depuis |
|-------|---------|---------------|
| Session courante | `GET /v2/me` | `IntraAuthService.fetchMe` (après login) |
| Recherche | `GET /v2/users?filter[login]=` | `IntraUserService` |
| Fiche complète | `GET /v2/users/{login}` | `IntraProfileService` |
| Cursus (secours) | `GET /v2/users/{id}/cursus_users` | `IntraProfileService` si pas embarqué |
| Projets | `GET /v2/users/{id}/projects_users` | `IntraProfileService` |
| Refresh token | `POST /oauth/token` | `IntraAuthService.refreshAccessToken` |
Requêtes **séquentielles** + cache profil 2 min + retry 429 : voir [etape-04-vue-profil.md](etape-04-vue-profil.md).
---
## 8. Chapitre VI — Rendu Git
| À pousser sur Vogosphere | À ne **pas** pousser |
|--------------------------|----------------------|
| Contenu de `swiftyCompanion/` | Dossier `docs/` (dépôt perso) |
| `README.md`, `.gitignore`, `.env.example` | Fichier `.env` |
| `swifty_companion/` (code Flutter) | `.env copy`, secrets dans l’historique Git |
Checklist : [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) · [swiftyCompanion/README.md](../../swiftyCompanion/README.md).
---
## 9. Script de démo (5–8 min)
1. `cd swiftyCompanion/swifty_companion` → `flutter test` (rapide).
2. `flutter run -d <device>` — montrer `.env` configuré **sans** afficher les secrets.
3. **Se connecter avec 42** — une fois.
4. Recherche login valide → **profil** : photo, champs, skills, projet failed si dispo.
5. Retour → **deuxième** recherche (même session).
6. Login invalide → message.
7. Mode avion → erreur réseau.
8. **Bonus :** DEBUG simuler token expiré → **Rechercher** → OK sans OAuth navigateur.
9. Rappeler : 2 vues, 1 token, `.env` ignoré par git.
---
## 10. Parcours pédagogique (docs perso uniquement)
| Étape | Guide |
|-------|--------|
| 0–1 | [00-reprise-et-etat.md](00-reprise-et-etat.md) |
| 2 OAuth | [etape-02-oauth-intra.md](etape-02-oauth-intra.md) |
| 3 Recherche | [etape-03-vue-recherche.md](etape-03-vue-recherche.md) |
| 4 Profil | [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| 5 Layout | [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| 6 Soutenance | [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| 7 Bonus | [etape-07-bonus-token.md](etape-07-bonus-token.md) |
| **Ce document** | **Cartographie sujet ↔ code pour la correction** |
---
## 11. Checklist « je n’ai rien oublié » (avant peer-eval)
- [ ] App OAuth Intra : scopes **public** + **projects**, redirect = `.env`
- [ ] `.env` présent sur la machine de démo, **absent** du Git école
- [ ] `flutter analyze` / `flutter test` OK
- [ ] Parcours nominal + erreurs + retour testés sur **device**
- [ ] Bonus refresh testé (DEBUG → Rechercher)
- [ ] Prêt à expliquer **où** est chaque critère du sujet (ce guide §3)
- [ ] SECRET régénéré si jamais exposé (chat, commit accidentel)
- [ ] Fichier **`.env copy`** retiré du Git — voir [audit-conformite-usage-api.md](audit-conformite-usage-api.md)
---
*Dernière alignement : sujet PDF v4 — traduction [swifty-companion.md](../sujets/swifty-companion.md).*