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

16 KiB
Raw Permalink Blame History

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

[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 :

  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)

  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

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