# 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.*