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>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
| Exigence | Cette étape |
|
||||
|----------|-------------|
|
||||
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet |
|
||||
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet — photo : URL API puis GET CDN avec Bearer (`_IntraProfileAvatar`) |
|
||||
| Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) |
|
||||
| Projets | Statuts **`finished`** et **`failed`** |
|
||||
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
|
||||
@@ -46,6 +46,14 @@ Accept: application/json
|
||||
|
||||
**Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth).
|
||||
|
||||
### Photo de profil
|
||||
|
||||
1. **URL** : extraite du JSON user (`_imageUrl` dans `profile_screen.dart`) — `image.link`, puis `image.versions`, puis `image_url`.
|
||||
2. **Fichier** : `GET` sur `cdn.intra.42.fr` avec `Authorization: Bearer` (`IntraAuthService.cdnImageHeaders`) — le CDN refuse souvent les requêtes sans token.
|
||||
3. **UI** : widget `_IntraProfileAvatar` (bytes en mémoire + `CircleAvatar`) ; sinon initiale du login.
|
||||
|
||||
Détail oral : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §5 « Photo de profil ».
|
||||
|
||||
### Skills — niveau et pourcentage
|
||||
|
||||
L’API renvoie un `level` flottant (ex. `8.42`) :
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
# Fiche soutenance orale — Swifty Companion
|
||||
|
||||
> **Lot 3** du [plan de préparation](00-plan-preparation-soutenance.md).
|
||||
> **Usage :** répétition à voix haute, ordre de démo, réponses courtes.
|
||||
> **Cartographie détaillée sujet ↔ code :** [guide-soutenance-realisation.md](guide-soutenance-realisation.md).
|
||||
> **Dépôt / `.env` / PC école :** [fiche-depot-soutenance.md](fiche-depot-soutenance.md).
|
||||
> **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 selon l’évaluateur — viser **8 min** de démo + questions.
|
||||
**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`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -14,244 +33,310 @@
|
||||
| Check | Action |
|
||||
|-------|--------|
|
||||
| Device | Téléphone chargé, USB débogage ou Wi‑Fi ADB OK |
|
||||
| `.env` | Présent dans `swiftyCompanion/swifty_companion/.env`, **pas** affiché à l’écran |
|
||||
| OAuth Intra | App avec scopes **public** + **projects**, redirect = `REDIRECT_URI` du `.env` |
|
||||
| Terminal prêt | Dossier `swiftyCompanion/swifty_companion` |
|
||||
| Git école | Pas de `.env` dans `git status` si on montre le repo |
|
||||
| `.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 (modèle) :**
|
||||
**À dire :**
|
||||
|
||||
> « Swifty Companion est une app Flutter qui se connecte une fois à l’API 42 en OAuth2, puis permet de chercher un login et d’afficher son profil : photo, coordonnées, niveau cursus, skills avec pourcentage, et projets validés ou en échec. Les secrets sont dans un `.env` local, ignoré par Git. J’ai deux écrans et un seul token réutilisé pour toutes les requêtes API. »
|
||||
> « 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 | Détail à mentionner si demandé |
|
||||
|-------|--------------------------------|
|
||||
| Sujet | Mobile Initiation v4 — API Intra **v2** |
|
||||
| Stack | Flutter / Dart, `flutter_appauth`, `http`, `flutter_dotenv` |
|
||||
| Entrée app | `lib/main.dart` → `SearchScreen` |
|
||||
| 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 fichier `.env` (~1 min)
|
||||
## 2. Sécurité et `.env` (~1 min)
|
||||
|
||||
### Ce que le sujet impose (éliminatoire)
|
||||
**Sujet :** secrets en `.env` local, **ignorés par Git** — exposition publique = échec direct.
|
||||
|
||||
- Identifiants **uniquement** dans `.env` **local**.
|
||||
- `.env` **ignoré par Git** — exposition publique = **échec direct**.
|
||||
- Le rendu Vogosphere contient **`.env.example`**, pas de vrais secrets.
|
||||
**Fichier exécuté :** `swiftyCompanion/swifty_companion/.env` (asset dans `pubspec.yaml`).
|
||||
|
||||
### Où est le fichier qui tourne
|
||||
| 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
|
||||
swiftyCompanion/swifty_companion/.env
|
||||
[Connexion 42] → access + refresh en RAM
|
||||
[Rechercher] → GET /users?filter[login]= avec Bearer access
|
||||
[Profil] → GET /users/{login}, projects_users, etc. même Bearer
|
||||
```
|
||||
|
||||
Chargé au démarrage : `dotenv.load()` dans `main.dart`, déclaré en asset dans `pubspec.yaml`.
|
||||
Recherche : ```82:95:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` → ```19:33:swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart```
|
||||
|
||||
### Les trois variables (à expliquer sans lire les valeurs à l’écran)
|
||||
### Quand l’access token expire (bonus sujet)
|
||||
|
||||
| Variable | Rôle | Où la trouver sur l’Intra |
|
||||
|----------|------|---------------------------|
|
||||
| `CLIENT_ID` | Identifiant de l’app OAuth | Champ **UID** |
|
||||
| `CLIENT_SECRET` | Échange code → tokens | Champ **SECRET** (privé) |
|
||||
| `REDIRECT_URI` | Retour navigateur → app | **URI de redirection** (ex. `com.example.swiftycompanion://callback`) |
|
||||
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```
|
||||
|
||||
**Scopes** (cochés sur l’Intra, pas dans `.env`) : **`public`** + **`projects`** — sans `projects`, les projets du profil renvoient **403**.
|
||||
**À 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. »
|
||||
|
||||
**Phrase clé :** « Le correcteur copie `.env.example` vers `.env` et met ses propres clés ; mon dépôt Git ne contient jamais le SECRET. »
|
||||
### 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```
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture (~1–2 min)
|
||||
## 5. Architecture des écrans et services
|
||||
|
||||
### Les deux vues
|
||||
### Deux vues
|
||||
|
||||
| Vue | Fichier | Rôle |
|
||||
|-----|---------|------|
|
||||
| **1 — Recherche** | `lib/screens/search_screen.dart` | Connexion OAuth, champ login, navigation vers profil |
|
||||
| **2 — Profil** | `lib/screens/profile_screen.dart` | Données agrégées, erreur + Réessayer |
|
||||
| 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``` |
|
||||
|
||||
### Couche services (flux données)
|
||||
### Chaîne d’appels
|
||||
|
||||
```text
|
||||
SearchScreen / ProfileScreen
|
||||
↓
|
||||
IntraUserService (recherche) IntraProfileService (profil)
|
||||
↓ ↓
|
||||
intra_http.dart (GET + gestion 401 / 429)
|
||||
↓
|
||||
IntraAuthService.instance (un seul access + refresh)
|
||||
→ IntraUserService / IntraProfileService
|
||||
→ intraApiGet (401, 429)
|
||||
→ IntraAuthService.instance
|
||||
```
|
||||
|
||||
| Fichier | Responsabilité |
|
||||
|---------|----------------|
|
||||
| `config/intra_config.dart` | URLs API v2 + lecture `.env` |
|
||||
| `services/intra_auth_service.dart` | OAuth, headers Bearer, refresh |
|
||||
| `services/intra_user_service.dart` | `GET /users?filter[login]=` |
|
||||
| `services/intra_profile_service.dart` | User + skills (cursus) + `projects_users` |
|
||||
| `services/intra_api_exception.dart` | Erreurs métier → messages UI |
|
||||
| `theme/swifty_ui.dart` | Scroll, largeurs max, AppBar commune |
|
||||
| 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``` |
|
||||
|
||||
### Un seul token OAuth (critère sujet)
|
||||
### Photo de profil (critère sujet n°4) — à savoir expliquer
|
||||
|
||||
1. **Une fois** : `authorizeAndExchangeCode` → access (+ refresh en bonus).
|
||||
2. **Ensuite** : chaque appel API utilise `authHeaders()` — **pas** de nouvelle fenêtre OAuth par recherche.
|
||||
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.
|
||||
|
||||
Singleton : `IntraAuthService.instance`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Démo guidée — commandes et parcours (~4–6 min)
|
||||
|
||||
### 4.1 Terminal — ce que fait chaque commande
|
||||
|
||||
Toujours depuis le package Flutter :
|
||||
|
||||
```bash
|
||||
cd ~/chemin/vers/swiftyCompanion/swifty_companion
|
||||
```
|
||||
|
||||
| Commande | À dire à l’oral |
|
||||
|----------|-----------------|
|
||||
| `flutter doctor` | Vérifie SDK Flutter, Android toolchain, device branché — preuve que l’environnement est prêt. |
|
||||
| `flutter pub get` | Télécharge les dépendances du `pubspec.yaml` (`flutter_appauth`, `http`, `dotenv`, …). |
|
||||
| `flutter analyze` | Analyse statique Dart — pas d’erreurs bloquantes. |
|
||||
| `flutter test` | Lance les tests unitaires / widget (`test/widget_test.dart`, `test/intra_oauth_tokens_test.dart` pour le bonus). |
|
||||
| `flutter devices` | Liste émulateur ou téléphone — on note l’id pour `-d`. |
|
||||
| `flutter run -d <device>` | Compile, installe et lance l’app en mode debug sur l’appareil. |
|
||||
|
||||
**Ordre recommandé devant l’évaluateur :** `flutter test` (rapide, visible) → `flutter run` si l’app n’est pas déjà lancée.
|
||||
|
||||
### 4.2 Parcours écran par écran
|
||||
|
||||
| Étape | Action | Ce que tu commentes |
|
||||
|-------|--------|---------------------|
|
||||
| 1 | Lancer l’app | Si `.env` manquant : message explicite sur l’écran recherche (`IntraConfig.isConfigured`). |
|
||||
| 2 | **Se connecter avec 42** | Flux OAuth : navigateur / Custom Tab → redirect vers l’app → token stocké en mémoire. |
|
||||
| 3 | Login valide (ex. un camarade ou toi) | `GET` filtre login → si trouvé, `Navigator.push` vers profil. |
|
||||
| 4 | **Profil** | Photo, ≥ 4 champs, carte skills (niveau + %), liste projets **finished** et **failed**. |
|
||||
| 5 | **Retour** (flèche) | Retour vue 1 — **session conservée** (pas besoin de reconnecter). |
|
||||
| 6 | Deuxième recherche | Prouve le token unique. |
|
||||
| 7 | Login invalide `zzzzinvalid` | Message clair, pas de crash, pas d’ouverture profil vide. |
|
||||
| 8 | Mode avion (recherche ou profil) | Erreur réseau ; sur profil, bouton **Réessayer**. |
|
||||
| 9 | (Option) Rotation / grand écran | Profil en 2 colonnes si large (`LayoutBuilder`, breakpoint 720 px). |
|
||||
|
||||
**Login de démo :** prévoir un login qui a des skills et au moins un projet failed visible.
|
||||
|
||||
---
|
||||
|
||||
## 5. Critères obligatoires — réponses « où c’est » (~2 min si questions)
|
||||
|
||||
Référence complète : [guide-soutenance-realisation.md §3](guide-soutenance-realisation.md#3-chapitre-v1--partie-obligatoire-9-critères).
|
||||
|
||||
| # | Critère | Réponse courte + fichier |
|
||||
|---|---------|---------------------------|
|
||||
| 1 | 2 vues | `search_screen.dart` + `profile_screen.dart` |
|
||||
| 2 | Toutes les erreurs | Exceptions dans `intra_api_exception.dart` ; catch dans services + UI (`search_screen`, `profile_screen` `_ErrorBody`) |
|
||||
| 3 | Profil seulement si login existe | Pas de navigation si liste users vide — `IntraUserNotFoundException` |
|
||||
| 4 | ≥ 4 infos + photo | `_ProfileHeader`, `_DetailsCard` — email, phone, location, wallet, niveau cursus, … |
|
||||
| 5 | Skills niveau + % | Depuis `cursus_users[].skills` (pas `/experiences`) — `IntraProfileService._parseSkillsFromCursus` |
|
||||
| 6 | Projets dont failed | `projects_users` — statuts `finished` et `failed` — scope `projects` |
|
||||
| 7 | Retour vue 1 | `Navigator.pop` + AppBar retour |
|
||||
| 8 | Layout moderne / flexible | `swifty_ui.dart` — scroll, `searchMaxWidth`, 2 colonnes profil |
|
||||
| 9 | Un token, pas un par requête | `IntraAuthService.instance` + `intra_http.dart` |
|
||||
|
||||
**Champs `—` sur le profil :** l’API masque certaines données selon la confidentialité — conforme au sujet.
|
||||
|
||||
---
|
||||
|
||||
## 6. Erreurs et cas limites — quoi montrer
|
||||
|
||||
| Cas | Démo | Message / comportement attendu |
|
||||
|-----|------|--------------------------------|
|
||||
| Login vide | Rechercher sans texte | « Saisis un login » |
|
||||
| Login inconnu | `zzzzinvalid` | Pas de profil |
|
||||
| Non connecté | (si évaluateur demande) tenter recherche sans OAuth | Invitation à se connecter |
|
||||
| Réseau | Mode avion | Message réseau |
|
||||
| 403 scopes | (expliquer) | Scopes `public` + `projects` sur l’app Intra |
|
||||
| 429 rate limit | (souvent pas en démo) | Retry dans `intra_http.dart` + cache profil 2 min |
|
||||
| 401 session | Après refresh impossible | « Session expirée… » — voir bonus |
|
||||
| Erreur chargement profil | (si réseau flaky) | Écran erreur + **Réessayer** |
|
||||
|
||||
Tu n’es pas obligé de tout rejouer : **2–3 cas** bien choisis suffisent souvent.
|
||||
|
||||
---
|
||||
|
||||
## 7. Bonus refresh token (~1–2 min)
|
||||
|
||||
### Flux à expliquer
|
||||
#### Deux étapes (API JSON ≠ fichier image)
|
||||
|
||||
```text
|
||||
Login → access + refresh en mémoire
|
||||
GET API → 401 Unauthorized
|
||||
→ POST /oauth/token (grant_type=refresh_token)
|
||||
→ nouveau access
|
||||
→ même GET rejoué une fois
|
||||
Si refresh échoue → IntraUnauthorizedException, l’utilisateur se reconnecte
|
||||
GET /v2/users/{login} → JSON avec "image": { "link", "versions": { large, medium, … } }
|
||||
↓
|
||||
GET https://cdn.intra.42.fr/users/…/….jpg → bytes JPEG (hébergement CDN)
|
||||
```
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---------|------|
|
||||
| `intra_auth_service.dart` | `refreshAccessToken()`, `_refreshInFlight` |
|
||||
| `intra_http.dart` | `intraApiGet` avec retry après refresh |
|
||||
| `intra_oauth_tokens.dart` | Parse JSON token — testé dans `intra_oauth_tokens_test.dart` |
|
||||
| É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` |
|
||||
|
||||
**Limite assumée :** tokens en **mémoire** — kill app = nouvelle connexion OAuth (acceptable si tu le dis).
|
||||
**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`.
|
||||
|
||||
### Démo DEBUG (build debug uniquement)
|
||||
**Pourquoi pas `authHeaders()` sur l’image ?** Ce helper envoie `Accept: application/json`, inadapté à un JPEG — d’où `cdnImageHeaders()` (Bearer seul).
|
||||
|
||||
1. Connexion OK, une recherche OK.
|
||||
2. Bouton **« DEBUG : simuler token expiré »** sur `SearchScreen` (`kDebugMode` — absent en release).
|
||||
3. **Rechercher** à nouveau → refresh silencieux → profil sans repasser par le navigateur.
|
||||
#### Script oral photo (~30 s)
|
||||
|
||||
**Si évaluateur en release :** montrer `flutter test` sur `intra_oauth_tokens_test.dart` + expliquer le flux §7.
|
||||
> « 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. »
|
||||
|
||||
**Bouton DEBUG au rendu :** **conservé** — visible uniquement en build debug (`kDebugMode`), utile pour prouver le bonus refresh ; absent en release. Voir [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md).
|
||||
|
||||
---
|
||||
|
||||
## 8. Questions fréquentes — réponses courtes
|
||||
#### FAQ correcteur
|
||||
|
||||
| Question | Réponse |
|
||||
|----------|---------|
|
||||
| Pourquoi pas `/experiences` pour les skills ? | Souvent **403** ; le sujet permet les données user / cursus — skills dans `cursus_users`. |
|
||||
| Pourquoi `projects` dans les scopes ? | Endpoint `projects_users` exige ce scope. |
|
||||
| Où est le redirect Android ? | `AndroidManifest.xml` — `RedirectUriReceiverActivity` ; `appAuthRedirectScheme` dans `build.gradle.kts`. |
|
||||
| Pourquoi des requêtes séquentielles ? | Éviter **429** Too Many Requests sur l’API 42. |
|
||||
| Cache profil ? | ~2 minutes en mémoire dans `IntraProfileService` — moins de hits API. |
|
||||
| Différence dépôt perso / école ? | Perso : `docs/` + travail ; école : **racine** = contenu de `swiftyCompanion/` seul. |
|
||||
| Où est la traduction du sujet ? | Dépôt perso `docs/sujets/` — pas dans Vogosphere. |
|
||||
| Tests ? | `widget_test` démarre l’app ; bonus : parse token OAuth. |
|
||||
| WSL vs téléphone ? | OAuth fiable sur **device/émulateur Android** ; voir stratégie VM école dans la piscine mobile. |
|
||||
| Que faire si SECRET exposé ? | Régénérer sur l’Intra, mettre à jour `.env` local, vérifier l’historique Git. |
|
||||
| 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. |
|
||||
|
||||
---
|
||||
|
||||
## 9. Script condensé (une page — à réciter)
|
||||
## 6. Démo guidée — terminal et parcours
|
||||
|
||||
1. **Intro** — Flutter, OAuth une fois, 2 vues, `.env` hors Git.
|
||||
2. **`flutter test`** — vert.
|
||||
3. **Connexion 42** — une fois.
|
||||
4. **Recherche OK** → profil (photo, champs, skills %, failed).
|
||||
5. **Retour** → 2ᵉ recherche sans reconnecter.
|
||||
6. **Login faux** + **mode avion**.
|
||||
7. **Bonus** — DEBUG token expiré → recherche OK.
|
||||
8. **Rappel** — 9 critères, un token, `public`+`projects`, `.env.example` dans le repo.
|
||||
### 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``` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Liens utiles (perso)
|
||||
## 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 |
|
||||
| [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | Checklist peer-eval |
|
||||
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | Détail refresh |
|
||||
| [swiftyCompanion/README.md](../../swiftyCompanion/README.md) | README correcteurs (rendu) |
|
||||
| [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 |
|
||||
|
||||
---
|
||||
|
||||
*Aligné sujet Swifty Companion v4 — lot 3 livré.*
|
||||
*Dernière mise à jour : photo CDN (Bearer), commentaires code, guide + README rendu.*
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
| Donnée affichée | Source API | Fichier UI |
|
||||
|-----------------|------------|------------|
|
||||
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
|
||||
| **Photo** | `image.link` ou `image_url` | `CircleAvatar` + `NetworkImage` — `_ProfileHeader` |
|
||||
| **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` |
|
||||
|
||||
Reference in New Issue
Block a user