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>
16 KiB
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
Dépôt /.env: 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
.envlocal, 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 »
-
SearchScreen._connect()appelleIntraAuthService.signIn()
→37:45:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart -
authorizeAndExchangeCode(flutter_appauth) : navigateur Intra → tu te connectes → redirectcom.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 -
Stockage en mémoire :
_accessToken+_refreshToken
→57:61:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart -
Vérification session :
GET /v2/meavec le même service
→128:133:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart -
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)
[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)
- Un
GETAPI renvoie 401 Unauthorized. intraApiGetappellerefreshAccessToken()une fois, puis rejoue le mêmeGET.
→19:24:swiftyCompanion/swifty_companion/lib/services/intra_http.dart- Le refresh fait
POST /oauth/tokenavecgrant_type=refresh_token.
→88:114:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart - 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 :
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 :
// 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)
- « Je me connecte une fois à l’API 42. »
- « Une recherche fonctionne : un seul access token, pas de nouvelle fenêtre OAuth. »
- « En build debug j’ai un bouton qui simule un access token expiré — il ne supprime pas le refresh token. » (montrer le bouton)
- « Je recherche à nouveau : l’API renvoie 401, mon
intra_httpdéclenche le refresh, puis relance le GET — le profil s’ouvre sans repasser par le navigateur. » - « 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
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)
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.frdans le champimage. Je parse d’abordlinkpuis lesversions. Ensuite je télécharge le fichier avec le même access token que pour l’API, viacdnImageHeaders. 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)
- Intro — Flutter, OAuth une fois, 2 vues,
.envhors Git (main.dart,intra_config.dart). flutter test.- Connexion —
signIn+AndroidManifestredirect. - Recherche → profil —
intra_user_service,intra_profile_service. - Retour → 2ᵉ recherche (même token).
- Erreurs — login faux, réseau.
- Bonus — expliquer access/refresh ; option DEBUG →
debugInvalidateAccessToken+intra_http401. - Rappel — 9 critères, scopes,
.env.exampleau rendu.
11. Liens doc perso
| Document | Usage |
|---|---|
| guide-soutenance-realisation.md | Tableau sujet ↔ fichiers détaillé |
| etape-07-bonus-token.md | Refresh approfondi |
| fiche-depot-soutenance.md | DEBUG §8, Vogosphere |
| swiftyCompanion/README.md | README correcteurs |
Dernière mise à jour : photo CDN (Bearer), commentaires code, guide + README rendu.