Files
swift_companion/docs/pedagogie/fiche-soutenance-orale.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

343 lines
16 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.
# Fiche soutenance orale — Swifty Companion
> **Usage :** répétition à voix haute, ordre de démo, **repères code** pour l’IDE.
> **Racine code à ouvrir en soutenance :** `swiftyCompanion/swifty_companion/`
> **Cartographie sujet ↔ code :** [guide-soutenance-realisation.md](guide-soutenance-realisation.md)
> **Dépôt / `.env` :** [fiche-depot-soutenance.md](fiche-depot-soutenance.md)
**Durée type peer-eval :** 5 à 15 min — viser **8 min** de démo + questions.
---
## Index rapide — où ouvrir quoi
| Sujet oral | Fichier (sous `swifty_companion/lib/` ou `test/`) |
|------------|---------------------------------------------------|
| Démarrage, `.env`, thème | `main.dart` |
| Secrets, URLs API v2 | `config/intra_config.dart` |
| Connexion OAuth, tokens, refresh | `services/intra_auth_service.dart` |
| GET + 401 → refresh + 429 | `services/intra_http.dart` |
| Recherche login | `services/intra_user_service.dart` + `screens/search_screen.dart` |
| Profil, skills, projets | `services/intra_profile_service.dart` + `screens/profile_screen.dart` |
| **Photo profil (URL + CDN Bearer)** | `profile_screen.dart` → `_imageUrl`, `_IntraProfileAvatar` · `intra_auth_service.dart` → `cdnImageHeaders` |
| Messages d’erreur | `services/intra_api_exception.dart` |
| Parse JSON token (bonus) | `services/intra_oauth_tokens.dart` + `test/intra_oauth_tokens_test.dart` |
| Layout responsive | `theme/swifty_ui.dart` |
| Redirect OAuth Android | `android/app/src/main/AndroidManifest.xml` |
| Bouton DEBUG (démo bonus) | `screens/search_screen.dart` (bloc `kDebugMode`) |
---
## Avant d’entrer en soutenance (5 min)
| Check | Action |
|-------|--------|
| Device | Téléphone chargé, USB débogage ou Wi‑Fi ADB OK |
| `.env` | `swiftyCompanion/swifty_companion/.env`, **pas** affiché à l’écran |
| OAuth Intra | Scopes **public** + **projects**, redirect = `.env` |
| Terminal | `cd swiftyCompanion/swifty_companion` |
| Git école | Pas de `.env` dans `git status` |
---
## 1. Introduction (~30 s)
**À dire :**
> « Swifty Companion : Flutter, OAuth2 Intra une fois, puis recherche par login et profil (photo, champs, skills avec %, projets validés et failed). Secrets dans `.env` local, ignoré par Git. Deux écrans, un seul access token pour toutes les requêtes API. »
| Point | Code |
|-------|------|
| Entrée + chargement `.env` | ```7:31:swiftyCompanion/swifty_companion/lib/main.dart``` |
| Vue 1 au lancement | `home: const SearchScreen()` — même fichier |
---
## 2. Sécurité et `.env` (~1 min)
**Sujet :** secrets en `.env` local, **ignorés par Git** — exposition publique = échec direct.
**Fichier exécuté :** `swiftyCompanion/swifty_companion/.env` (asset dans `pubspec.yaml`).
| Variable | Intra 42 |
|----------|----------|
| `CLIENT_ID` | UID |
| `CLIENT_SECRET` | SECRET |
| `REDIRECT_URI` | URI de redirection |
**Scopes** (Intra, pas dans `.env`) : `public` + `projects`.
| Rôle | Code |
|------|------|
| Lecture `.env` + `isConfigured` | ```9:14:swiftyCompanion/swifty_companion/lib/config/intra_config.dart``` |
| `dotenv.load` au boot | ```7:9:swiftyCompanion/swifty_companion/lib/main.dart``` |
| Message si `.env` incomplet | ```109:118:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Modèle versionné (rendu) | `swiftyCompanion/.env.example` |
---
## 3. Connexion 42 — OAuth2 pas à pas (~1–2 min)
### Ce qui se passe quand tu appuies sur « Se connecter avec 42 »
1. **`SearchScreen._connect()`** appelle **`IntraAuthService.signIn()`**
→ ```37:45:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
2. **`authorizeAndExchangeCode`** (`flutter_appauth`) : navigateur Intra → tu te connectes → redirect `com.example.swiftycompanion://callback` → l’app reçoit les tokens.
→ ```40:50:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
Scopes : `public`, `projects` — ```46:46:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
3. **Stockage en mémoire** : `_accessToken` + `_refreshToken`
→ ```57:61:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
4. **Vérification session** : `GET /v2/me` avec le même service
→ ```128:133:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
5. **Redirect Android** (schéma + host)
→ ```26:38:swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml```
**Critère sujet n°9 :** après cette étape, **plus de OAuth** pour chaque recherche — seulement `Authorization: Bearer …`
→ ```117:125:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` (`authHeaders`)
**Erreurs OAuth** (annulation, plateforme) : ```50:68:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
---
## 4. Tokens : access, refresh, et bouton DEBUG (à maîtriser pour le bonus)
> Section la plus importante si l’évaluateur demande le **bonus** ou le bouton **DEBUG**.
### Vocabulaire simple (à réciter)
| Jeton | Rôle | Analogie |
|-------|------|----------|
| **Access token** | Prouve à l’API que tu es autorisé ; envoyé sur chaque `GET` / `POST` API | **Badge** d’entrée — durée courte |
| **Refresh token** | Permet d’obtenir un **nouveau** access token **sans** rouvrir le navigateur OAuth | **Clé de renouvellement** du badge |
Les deux sont obtenus **une fois** au login OAuth, puis gardés dans **`IntraAuthService.instance`** (singleton).
### Flux normal (sans bouton DEBUG)
```text
[Connexion 42] → access + refresh en RAM
[Rechercher] → GET /users?filter[login]= avec Bearer access
[Profil] → GET /users/{login}, projects_users, etc. même Bearer
```
Recherche : ```82:95:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` → ```19:33:swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart```
### Quand l’access token expire (bonus sujet)
1. Un `GET` API renvoie **401 Unauthorized**.
2. **`intraApiGet`** appelle **`refreshAccessToken()`** une fois, puis **rejoue** le même `GET`.
→ ```19:24:swiftyCompanion/swifty_companion/lib/services/intra_http.dart```
3. Le refresh fait **`POST /oauth/token`** avec `grant_type=refresh_token`.
→ ```88:114:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
4. Si le refresh échoue → **`IntraUnauthorizedException`** (« Session expirée… »)
→ ```25:27:swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart```
**À dire :** « Le bonus n’est pas un second OAuth à chaque fois : c’est le refresh automatique sur 401, puis la même requête est relancée. »
### Le bouton « DEBUG : simuler token expiré » — qu’est-ce que ça fait **vraiment** ?
| Question | Réponse courte |
|----------|----------------|
| C’est demandé par le sujet ? | **Non.** C’est un **raccourci de démo** en développement. |
| C’est de la triche ? | **Non.** Il ne contourne pas l’API : il force la même situation qu’un access token périmé, pour tester le **vrai** flux refresh. |
| Visible en soutenance release ? | **Non.** Compilé seulement si `kDebugMode` (ex. `flutter run`). Absent en `flutter build apk --release`. |
**Mécanisme en 3 lignes de code :**
```143:146:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart
void debugInvalidateAccessToken() {
if (!canRefresh) return;
_accessToken = 'invalid_access_token_for_refresh_test';
}
```
- Le **refresh token n’est pas touché**.
- L’**access token** devient une valeur volontairement fausse → la prochaine API répond **401** → le chemin **bonus** (§ ci-dessus) s’exécute.
**UI du bouton :**
```189:205:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
if (kDebugMode) ...[
...
_auth.debugInvalidateAccessToken();
...
child: const Text('DEBUG : simuler token expiré'),
```
### Script oral démo DEBUG (~45 s)
1. « Je me connecte une fois à l’API 42. »
2. « Une recherche fonctionne : un seul access token, pas de nouvelle fenêtre OAuth. »
3. « En build debug j’ai un bouton qui **simule** un access token expiré — il ne supprime pas le refresh token. » *(montrer le bouton)*
4. « Je recherche à nouveau : l’API renvoie 401, mon `intra_http` déclenche le refresh, puis relance le GET — le profil s’ouvre sans repasser par le navigateur. »
5. « En production le bouton n’existe pas ; le mécanisme reste le même quand le token expire vraiment. »
**Si pas de téléphone / pas de debug :** `flutter test` sur le parse token
→ `swiftyCompanion/swifty_companion/test/intra_oauth_tokens_test.dart`
→ ```12:20:swiftyCompanion/swifty_companion/lib/services/intra_oauth_tokens.dart```
**Limite à assumer :** tokens en **mémoire** — fermer l’app = nouvelle connexion OAuth (`signOut` / kill)
→ ```64:67:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
---
## 5. Architecture des écrans et services
### Deux vues
| Vue | Fichier | Code clé |
|-----|---------|----------|
| Recherche + OAuth | `screens/search_screen.dart` | Classe ```14:19:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Profil | `screens/profile_screen.dart` | `fetchProfile` ```37:38:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
### Chaîne d’appels
```text
SearchScreen / ProfileScreen
→ IntraUserService / IntraProfileService
→ intraApiGet (401, 429)
→ IntraAuthService.instance
```
| Service | Fichier | Lignes utiles |
|---------|---------|---------------|
| Profil agrégé | `intra_profile_service.dart` | `fetchProfile` ~71 ; skills ~173 ; projets ~222 ; cache TTL ~65-66 |
| Erreurs typées | `intra_api_exception.dart` | fichier entier |
| Layout | `swifty_ui.dart` | `wideBreakpoint` ```13:13:swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart``` ; profil 2 col. ```103:103:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
### Photo de profil (critère sujet n°4) — à savoir expliquer
Le PDF demande **quatre détails + profile picture**. L’encadré confidentialité permet de ne pas afficher une donnée **si l’API ne la fournit pas** ; ton code **tente** d’afficher la photo dès qu’une URL existe.
#### Deux étapes (API JSON ≠ fichier image)
```text
GET /v2/users/{login} → JSON avec "image": { "link", "versions": { large, medium, … } }
↓
GET https://cdn.intra.42.fr/users/…/….jpg → bytes JPEG (hébergement CDN)
```
| Étape | Qui | Code |
|-------|-----|------|
| 1. Lire l’URL dans le JSON | `ProfileScreen._imageUrl` | ```65:83:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| 2. Télécharger l’image | `_IntraProfileAvatar._load` | ```222:236:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| 3. Header Bearer (même access token) | `IntraAuthService.cdnImageHeaders` | ```128:136:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` |
| 4. Affichage | `CircleAvatar` + `MemoryImage` | ```244:251:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| En-tête profil | `_ProfileHeader` passe `imageUrl` | cherche `_ProfileHeader` dans `profile_screen.dart` |
**Pourquoi pas `NetworkImage(url)` seul ?** Le CDN 42 renvoie souvent **403** sans `Authorization: Bearer`. Ce n’est **pas** un nouveau OAuth : c’est le **même** access token que pour `intraApiGet`.
**Pourquoi pas `authHeaders()` sur l’image ?** Ce helper envoie `Accept: application/json`, inadapté à un JPEG — d’où `cdnImageHeaders()` (Bearer seul).
#### Script oral photo (~30 s)
> « Le sujet demande la photo de profil. L’API me donne une URL sur `cdn.intra.42.fr` dans le champ `image`. Je parse d’abord `link` puis les `versions`. Ensuite je télécharge le fichier avec le même access token que pour l’API, via `cdnImageHeaders`. Si ça échoue ou s’il n’y a pas d’URL, j’affiche l’initiale du login — c’est prévu par l’encadré confidentialité du sujet. »
#### FAQ correcteur
| Question | Réponse |
|----------|---------|
| Un token par image ? | **Non** — réutilisation du access token déjà obtenu au login. |
| Régénérer l’app OAuth pour la photo ? | **Non** — la photo dépend du JSON + CDN, pas du SECRET (sauf si OAuth cassé). |
| `public` suffit pour la photo ? | **Oui** — la photo est dans les données user publiques. |
---
## 6. Démo guidée — terminal et parcours
### Commandes (depuis `swiftyCompanion/swifty_companion/`)
| Commande | Oral |
|----------|------|
| `flutter pub get` | Dépendances |
| `flutter analyze` | *(orthographe anglaise)* analyse statique |
| `flutter test` | `test/widget_test.dart` + `intra_oauth_tokens_test.dart` |
| `flutter devices` / `flutter run -d …` | Device + lancement |
### Parcours écran
| Étape | Code à citer si demandé |
|-------|-------------------------|
| Connexion | `_connect` → `signIn` (§3) |
| Navigation profil | ```91:94:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Erreur profil + Réessayer | `_ErrorBody` dans `profile_screen.dart` |
| Login inconnu | `IntraUserNotFoundException` dans `intra_user_service.dart` |
| Déconnexion | `_disconnect` → `signOut` ```74:79:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
---
## 7. Critères obligatoires — repères code
| # | Critère | Où montrer |
|---|---------|------------|
| 1 | 2 vues | `search_screen.dart` + `profile_screen.dart` |
| 2 | Erreurs | `intra_api_exception.dart` ; catches `search_screen` / `profile_screen` |
| 3 | Profil si login existe | Liste vide → exception, pas de `Navigator.push` — `intra_user_service.dart` ~53-55 |
| 4 | ≥4 champs + photo | `_DetailsCard` + `_imageUrl` / `_IntraProfileAvatar` — `profile_screen.dart` (§5 photo) |
| 5 | Skills niveau + % | `_parseSkillsFromCursus` ```173:207:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
| 6 | Projets + failed | `_parseProjects` ```222:246:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
| 7 | Retour vue 1 | `showBack: true` sur AppBar profil ; `Navigator.pop` implicite |
| 8 | Layout flexible | `ResponsiveScrollBody`, `LayoutBuilder` wide 720px |
| 9 | Un token | `IntraAuthService.instance` ; pas de `signIn` dans `_search` |
**Champs `—` :** confidentialité OAuth — conforme sujet.
---
## 8. Erreurs — quoi montrer
| Cas | Code / message |
|-----|----------------|
| Login vide | `intra_user_service.dart` ~20-22 |
| Non connecté | `IntraNotAuthenticatedException` |
| Réseau | `IntraNetworkException` dans user/profile services |
| 403 scopes | `IntraForbiddenException` |
| 429 | Retry ```27:32:swiftyCompanion/swifty_companion/lib/services/intra_http.dart``` |
| Session morte | `IntraUnauthorizedException` après échec refresh |
---
## 9. Questions fréquentes
| Question | Réponse + code |
|----------|----------------|
| Pourquoi pas `/experiences` ? | Skills depuis `cursus_users` — `_parseSkillsFromCursus` |
| Pourquoi scope `projects` ? | `projects_users` ; sinon 403 |
| Pourquoi le bouton DEBUG ? | Démo du bonus refresh en debug — §4 |
| Régénérer SECRET pour la photo ? | **Non** — voir §5 photo (CDN + Bearer) |
| Comment charge-t-on la photo ? | `_imageUrl` puis `_IntraProfileAvatar` + `cdnImageHeaders` |
| Tests bonus ? | `test/intra_oauth_tokens_test.dart` |
---
## 10. Script condensé (1 page)
1. Intro — Flutter, OAuth une fois, 2 vues, `.env` hors Git (`main.dart`, `intra_config.dart`).
2. `flutter test`.
3. Connexion — `signIn` + `AndroidManifest` redirect.
4. Recherche → profil — `intra_user_service`, `intra_profile_service`.
5. Retour → 2ᵉ recherche (même token).
6. Erreurs — login faux, réseau.
7. Bonus — expliquer access/refresh ; option DEBUG → `debugInvalidateAccessToken` + `intra_http` 401.
8. Rappel — 9 critères, scopes, `.env.example` au rendu.
---
## 11. Liens doc perso
| Document | Usage |
|----------|--------|
| [guide-soutenance-realisation.md](guide-soutenance-realisation.md) | Tableau sujet ↔ fichiers détaillé |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | Refresh approfondi |
| [fiche-depot-soutenance.md](fiche-depot-soutenance.md) | DEBUG §8, Vogosphere |
| [swiftyCompanion/README.md](../../swiftyCompanion/README.md) | README correcteurs |
---
*Dernière mise à jour : photo CDN (Bearer), commentaires code, guide + README rendu.*