Compare commits

2 Commits

Author SHA1 Message Date
d8f2235e87 brancheok 2026-10-21 12:02:55 +02:00
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
29 changed files with 2502 additions and 2 deletions

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# Swifty Companion — dépôt de travail
Espace **perso** : sujet traduit, pédagogie, notes. Le rendu à l’école ne contient **pas** tout ce dépôt.
## Deux dépôts Git distincts
| Rôle | Où | Contenu typique |
|------|-----|-----------------|
| **Travail** (ce repo) | Racine `swifty_companion/` | `docs/`, historique, éventuellement une copie du code |
| **École** (Intra / Vogosphere) | Racine = contenu de **`swiftyCompanion/`** | App mobile + `README.md` + `.gitignore` + `.env.example` uniquement |
Le sous-dossier [`swiftyCompanion/`](swiftyCompanion/) est le **paquet de rendu** : README et `.gitignore` **autonomes**, prêts pour le futur dépôt école.
### Pousser vers l’école (quand le code est prêt)
**Option A — Git dans le sous-dossier** (recommandé en local) :
```bash
cd swiftyCompanion
git init
git remote add origin <url-vogosphere-swifty_companion>
# premier push : uniquement les fichiers de ce dossier
```
Dans le dépôt de **travail**, ignorer le `.git` imbriqué (déjà dans `.gitignore` racine) pour éviter un repo dans un repo par accident.
**Option B — Clone Vogosphere vide** puis copier le contenu de `swiftyCompanion/` (sans `docs/`) à la racine du clone.
Ne pas pousser : `docs/`, traductions, PDF, dépôt perso.
## Documentation (travail uniquement)
| Document | Lien |
|----------|------|
| Sujet FR (v4) | [docs/sujets/swifty-companion.md](docs/sujets/swifty-companion.md) |
| PDF officiel | [docs/sujets/originaux/swifty_companion_subject.pdf](docs/sujets/originaux/swifty_companion_subject.pdf) |
| Plan pas à pas | [docs/pedagogie/README.md](docs/pedagogie/README.md) |
| Index doc | [docs/README.md](docs/README.md) |
| Piscine mobile (référence) | [../piscine_mobile/README.md](../piscine_mobile/README.md) |
## Rendu école
Tout ce qui concerne correcteurs et soutenance : **[swiftyCompanion/README.md](swiftyCompanion/README.md)** (à lire depuis le dépôt Intra une fois poussé).
## Reprise du projet
**État actuel :** app Flutter dans `swiftyCompanion/swifty_companion/`, partie obligatoire codée.
→ [docs/pedagogie/00-reprise-et-etat.md](docs/pedagogie/00-reprise-et-etat.md) · **correction :** [guide-soutenance-realisation.md](docs/pedagogie/guide-soutenance-realisation.md)
## Démarrage
1. Lire le [sujet traduit](docs/sujets/swifty-companion.md).
2. Suivre [00-reprise-et-etat.md](docs/pedagogie/00-reprise-et-etat.md) puis le [plan pédagogique](docs/pedagogie/README.md).
3. Coder uniquement dans `swiftyCompanion/` ([README rendu](swiftyCompanion/README.md)).

25
docs/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Documentation — Swifty Companion
Documentation du projet, sur le modèle de [piscine_mobile/docs](../piscine_mobile/docs/README.md).
## Sujet officiel
→ [sujets/README.md](sujets/README.md)
| Document | Lien |
|----------|------|
| Traduction française (v4) | [sujets/swifty-companion.md](sujets/swifty-companion.md) |
| PDF Intra | [sujets/originaux/swifty_companion_subject.pdf](sujets/originaux/swifty_companion_subject.pdf) |
## Pédagogie
| Document | Lien |
|----------|------|
| **Reprise / état du projet** | [pedagogie/00-reprise-et-etat.md](pedagogie/00-reprise-et-etat.md) |
| Plan des étapes | [pedagogie/README.md](pedagogie/README.md) |
## Dépôt de rendu 42 (hors dépôt de travail)
Le code et le README correcteurs vivent dans **`swiftyCompanion/`** — racine du futur dépôt Intra, séparé de ce dépôt perso.
→ [../swiftyCompanion/README.md](../swiftyCompanion/README.md) · workflow : [../README.md](../README.md#deux-dépôts-git-distincts)

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,73 @@
# Plan de préparation — dépôt soutenance, doc et code
> **Méthode :** pour chaque lot, l’agent décrit ce qu’il compte faire → **tu valides ou ajustes** → on exécute → on coche.
**Dernière mise à jour :** lot 4 livré (commentaires `lib/` + DEBUG conservé).
---
## Décisions validées (lot 0)
| Point | Décision |
|-------|----------|
| Branche `soutenance` | Même contenu que rendu école ; `main` = travail perso ; présentation portable sur `soutenance` |
| PC école | Clone perso → copie `swiftyCompanion/` → supprimer repo perso → `git init` rendu → push Vogosphere (détail §6 fiche dépôt) |
| `.env` | **Uniquement** `swifty_companion/.env` en local ; **jamais** dans Git ; `.env.example` versionné (sujet : échec direct si secrets publics) |
| Fiche orale (lot 3) | **Déroulé de soutenance** détaillé (sujet, points critiques, réalisation, commandes expliquées) — un fichier structuré logiquement |
| Commentaires code | **Français** — sujet ne l’interdit pas |
| Bouton DEBUG | **Garder** — aide à démontrer le bonus refresh en `flutter run` ; absent en release (`kDebugMode`) — [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) |
---
## Où vit quoi
| Zone | Chemin | Git |
|------|--------|-----|
| Travail | racine `swifty_companion/` | Gitea privé (`main`) |
| Rendu / soutenance | `swiftyCompanion/` | Vogosphere + branche `soutenance` |
| Oral long | `docs/pedagogie/` | perso uniquement |
---
## Ordre des lots
| Phase | Lot | Statut |
|-------|-----|--------|
| A | 0 Cadrage | ✅ Validé |
| B | 1 Dépôt + branche | ✅ [fiche-depot-soutenance.md](fiche-depot-soutenance.md) |
| C | 2 README rendu autonome | ✅ fait |
| D | 3 Fiche soutenance orale | ✅ [fiche-soutenance-orale.md](fiche-soutenance-orale.md) |
| E | 4 Commentaires `lib/` | ✅ fait |
---
## Lot 2 — Livré
- `swiftyCompanion/README.md` réécrit (autonome, sans lien `docs/`).
- `.env.example` commenté (racine + `swifty_companion/.env.example`).
- `swifty_companion/README.md` : pointeur court vers le README racine.
## Lot 3 — Livré
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — déroulé oral, commandes commentées, démo, FAQ.
---
## Lot 4 — Livré
- Commentaires / dartdoc en **français** dans `swiftyCompanion/swifty_companion/lib/`.
- Bouton **DEBUG** conservé (`kDebugMode`) pour la démo du bonus refresh — pas présent en APK release.
**Plan préparation :** tous les lots 0–4 sont cochés ; prochaine action opérationnelle = branche `soutenance` + push Vogosphere ([fiche-depot-soutenance.md](fiche-depot-soutenance.md)).
---
## Suivi
| Lot | Fait ? |
|-----|--------|
| 0 | ✅ |
| 1 | ✅ |
| 2 | ✅ |
| 3 | ✅ |
| 4 | ✅ |

View File

@@ -0,0 +1,109 @@
# Reprise du projet — où nous en sommes
> Dernière mise à jour : étape 7 bonus (refresh token) codée.
## En bref
| | |
|---|---|
| **Fait** | Structure dépôt perso, sujet FR, paquet rendu, **projet Flutter** (`swifty_companion/`) |
| **Fait** | OAuth + token + `/v2/me` (login connecté) |
| **Fait** | Vue 1 recherche + vue 2 profil (skills, projets, ≥ 4 champs) |
| **Prochaine action** | **Rendu Vogosphere** + test manuel bonus (token invalide) |
## Arborescence actuelle
```
swifty_companion/ ← dépôt Git de TRAVAIL (perso)
├── README.md ← explique les 2 dépôts Git
├── .gitignore ← ignore docs + swiftyCompanion/.git/
├── docs/
│ ├── README.md
│ ├── sujets/
│ │ ├── swifty-companion.md ← sujet traduit (référence)
│ │ ├── originaux/swifty_companion_subject.pdf
│ │ └── captures/ ← visuels PDF (couverture uniquement)
│ └── pedagogie/
│ ├── README.md ← plan des étapes
│ └── 00-reprise-et-etat.md ← ce fichier
└── swiftyCompanion/ ← futur dépôt ÉCOLE (racine Vogosphere)
├── README.md
├── .env.example
└── swifty_companion/ ← projet Flutter (lib/, android/, pubspec.yaml)
```
## Décisions déjà prises
1. **Deux dépôts Git** : tout `docs/` et la traduction restent dans le repo perso ; l’Intra ne reçoit que le contenu de `swiftyCompanion/`.
2. **Stack** : Flutter, `flutter_dotenv`, `flutter_appauth`, `http`.
3. **Secrets** : `.env` local uniquement, jamais commité (modèle dans `swiftyCompanion/.env.example`).
4. **Redirect OAuth mobile** : `com.example.swiftycompanion://callback` (pas `localhost` sur téléphone physique).
5. **Sujet PDF** : pas de maquettes d’écrans — l’UI suit la liste des critères obligatoires ([sujet FR](../sujets/swifty-companion.md)).
## Avancement par étape
| Étape | Contenu | Statut |
|-------|---------|--------|
| **0** | Structure, traduction sujet, séparation travail / école, README rendu | ✅ **Terminé** |
| **1** | Projet Flutter `swiftyCompanion/swifty_companion/` | ✅ **Terminé** |
| **2** | OAuth2 (`IntraAuthService`, `/v2/me`) | ✅ **Validé** |
| **3** | Vue 1 : `SearchScreen` + `IntraUserService` + navigation profil | ✅ **Codé** — tester sur téléphone |
| **4** | Vue 2 : `IntraProfileService` + profil (skills, projets, champs) | ✅ **Codé** — tester sur téléphone |
| **5** | Retour vue 1 + layout adaptatif (`swifty_ui.dart`) | ✅ **Codé** — [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| **6** | Checklist soutenance + rendu école | ✅ **Guide** — [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| **7** | Bonus : refresh token à expiration | ✅ **Codé** — [etape-07-bonus-token.md](etape-07-bonus-token.md) |
## Code applicatif (`swifty_companion/lib/`)
| Fichier | Rôle |
|---------|------|
| `config/intra_config.dart` | `.env`, URLs OAuth / API |
| `services/intra_auth_service.dart` | OAuth + refresh token + `GET /v2/me` |
| `services/intra_oauth_tokens.dart` | Parse réponse `/oauth/token` |
| `services/intra_user_service.dart` | `GET /v2/users?filter[login]=` |
| `services/intra_profile_service.dart` | Profil : user, cursus_users, projects_users |
| `services/intra_api_exception.dart` | Erreurs métier |
| `screens/search_screen.dart` | **Vue 1** |
| `screens/profile_screen.dart` | **Vue 2** |
| `theme/swifty_ui.dart` | Layout responsive partagé |
| `main.dart` | `home: SearchScreen()` |
## Reprendre le travail (checklist)
1. Ouvrir le dépôt : `~/projets/42School/swifty_companion`
2. Relire le [sujet traduit](../sujets/swifty-companion.md) (section obligatoire).
3. Vérifier Flutter : `flutter doctor` et `flutter devices`.
4. Lancer l’app (après modification `.env`, relancer `flutter run`) :
```bash
cd swiftyCompanion/swifty_companion
flutter run -d <device>
```
5. Tester le parcours : OAuth → recherche → profil → retour → erreurs réseau.
## Fichiers utiles selon le contexte
| Tu veux… | Fichier |
|----------|---------|
| **Préparer la correction (sujet ↔ fichiers)** | **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** |
| Comprendre le sujet | [docs/sujets/swifty-companion.md](../sujets/swifty-companion.md) |
| Plan complet des étapes | [docs/pedagogie/README.md](README.md) |
| Vue profil (étape 4) | [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| Workflow Git travail / école | [README racine](../../README.md) |
| Soutenance / peer-eval | [swiftyCompanion/README.md](../../swiftyCompanion/README.md) |
## Guides détaillés
| Fichier | Statut |
|---------|--------|
| [etape-02-oauth-intra.md](etape-02-oauth-intra.md) | ✅ |
| [etape-03-vue-recherche.md](etape-03-vue-recherche.md) | ✅ |
| [etape-04-vue-profil.md](etape-04-vue-profil.md) | ✅ |
| [etape-05-navigation-layout.md](etape-05-navigation-layout.md) | ✅ |
| [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | ✅ |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | ✅ piste bonus |
## Notes pour la session suivante
- Partie obligatoire : prête pour peer-eval si la checklist étape 6 est OK sur device.
- `git init` dans `swiftyCompanion/` quand tu pousses sur Vogosphere (voir README racine).
- Bonus refresh token : étape 7 si tu as le temps après rendu.

58
docs/pedagogie/README.md Normal file
View File

@@ -0,0 +1,58 @@
# Pédagogie — Swifty Companion
Parcours pour construire le projet **étape par étape**.
## Reprise rapide
→ **[00-reprise-et-etat.md](00-reprise-et-etat.md)** — état du projet.
→ **[00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)** — plan (valider lot par lot).
→ **[fiche-depot-soutenance.md](fiche-depot-soutenance.md)** — branche `soutenance`, `.env`, push PC école, bouton DEBUG.
→ **[fiche-soutenance-orale.md](fiche-soutenance-orale.md)** — déroulé soutenance, commandes, démo, FAQ.
→ **[protocole-test-manuel-device.md](protocole-test-manuel-device.md)** — checklist téléphone + FAQ (hidden, cache, logout, skills).
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — cartographie sujet ↔ code pour la correction.
## Prérequis
Base via la [piscine mobile](../../../piscine_mobile/README.md) sur le même disque :
- [Flutter — Ubuntu natif](../../../piscine_mobile/docs/pedagogie/flutter/09-ubuntu-natif-dev-android.md)
- [Stratégie VM école / correction](../../../piscine_mobile/docs/pedagogie/flutter/07-strategie-wsl-vm-ecole-correction.md)
## Étapes du projet
| Étape | Objectif | Statut |
|-------|----------|--------|
| **0** | Dépôt travail + `docs/`, sujet FR, paquet `swiftyCompanion/` | ✅ **Fait** — [00-reprise-et-etat.md](00-reprise-et-etat.md) |
| **1** | Projet `swiftyCompanion/swifty_companion/` + `flutter run` | ✅ **Fait** |
| **2** | OAuth2 Intra : token + `/v2/me` | ✅ **Fait** — [etape-02-oauth-intra.md](etape-02-oauth-intra.md) |
| **3** | **Vue 1** : recherche par login + erreurs | ✅ **Codé** — [etape-03-vue-recherche.md](etape-03-vue-recherche.md) |
| **4** | **Vue 2** : profil (≥ 4 champs, skills, projets failed) | ✅ **Codé** — [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| **5** | Navigation retour + layout responsive | ✅ **Codé** — [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| **6** | Tests + checklist peer-eval + rendu Git école | ✅ **Guide** — [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| **7** | **Bonus** : refresh token à l’expiration | ✅ **Codé** — [etape-07-bonus-token.md](etape-07-bonus-token.md) |
README correcteurs (dépôt école) : [swiftyCompanion/README.md](../../swiftyCompanion/README.md)
## Sujet de référence
→ [Traduction FR v4](../sujets/swifty-companion.md) · PDF : [originaux/swifty_companion_subject.pdf](../sujets/originaux/swifty_companion_subject.pdf)
## Guides détaillés
| Fichier | Statut |
|---------|--------|
| [00-reprise-et-etat.md](00-reprise-et-etat.md) | ✅ À jour |
| [etape-02-oauth-intra.md](etape-02-oauth-intra.md) | ✅ Guide étape 2 |
| [etape-03-vue-recherche.md](etape-03-vue-recherche.md) | ✅ Guide étape 3 |
| [etape-04-vue-profil.md](etape-04-vue-profil.md) | ✅ Guide étape 4 |
| [etape-05-navigation-layout.md](etape-05-navigation-layout.md) | ✅ Guide étape 5 |
| [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | ✅ Guide étape 6 |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | ✅ Bonus refresh |
| **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** | ✅ **Guide correction / soutenance** |
Demande dans le chat l’étape à ouvrir : le guide correspondant sera rédigé au moment où tu l’attaques.

View File

@@ -0,0 +1,109 @@
# Audit conformité usage API 42 — Swifty Companion
> **Date :** audit local dépôt de travail
> **Contraintes respectées :** aucune lecture du fichier `.env` local actuel ; **aucune requête** vers `api.intra.42.fr`.
Objectif : vérifier si le **code et le dépôt** peuvent expliquer une **extraction massive** (~140k profils) du type décrit par le mail 42 — ou seulement un usage **pédagogique ponctuel**.
---
## 1. Verdict global
| Question | Réponse |
|----------|---------|
| Le **code Flutter** ressemble-t-il à un scraper massif ? | **Non** |
| Usage prévu du sujet Swifty Companion ? | **Oui** (1 login → 1 profil) |
| Risque d’être l’**auteur principal** de la fuite nationale décrite ? | **Très improbable** si tu n’as pas lancé d’autres scripts |
| Point **sécurité** à corriger dans **ton** Git ? | **Oui** — voir §4 |
---
## 2. Analyse du code (`swiftyCompanion/swifty_companion/lib/`)
### Appels API présents
| Fichier | Comportement | Volume typique |
|---------|--------------|----------------|
| `intra_user_service.dart` | `GET /v2/users?filter[login]=<un login>` | **1 utilisateur** par recherche |
| `intra_profile_service.dart` | `GET /users/{login}`, évent. `cursus_users`, `projects_users` (page 100) | **1 profil** par navigation |
| `intra_auth_service.dart` | OAuth + `GET /me` + refresh token | Session |
| `intra_http.dart` | GET avec retry 429, refresh sur 401 | Par requête ci-dessus |
### Absent du dépôt (recherché)
- Pas de script `sh` / `py` d’export massif
- Pas de boucle sur campus / pagination `page[number]` sur tous les users
- Pas de `filter[campus]`, pas de dump CSV/JSON de milliers de profils
- Pas de stockage fichier des profils (cache mémoire **2 min**, **1 id** à la fois)
**Conclusion technique :** l’application ne peut pas, **seule**, extraire 140k profils sans modification majeure du code.
---
## 3. Lien avec le mail école
Le mail décrit :
- extraction **automatisée** à très grande échelle ;
- **multi-campus**, candidats + étudiants ;
- via un **compte légitime** (pas forcément ton app OAuth).
Ton projet :
- recherche **manuelle** par login ;
- quelques requêtes par profil ;
- aligné sur le sujet (« consulter un pair »).
**Ce n’est pas le même profil d’usage**, sauf si tu as **en plus** utilisé un autre outil, un script perso, ou partagé des données.
---
## 4. Risque sécurité — dépôt Git (hors fuite massive)
Audit Git **sans afficher les valeurs** des secrets :
| Élément | État |
|---------|------|
| `swifty_companion/.env` | **Ignoré** par `.gitignore` (OK) |
| `swifty_companion/.env copy` | **Versionné dans Git** (commit `06fe43b`) |
| Contenu versionné | Présence de `CLIENT_ID` et `CLIENT_SECRET` dans ce fichier (valeurs **non** recopiées dans ce document) |
| Remote | `origin` → **Gitea privé** perso (pas Vogosphere 42) |
**Risque :** si le dépôt est **privé** et que **toi seul** y as accès, l’exposition des clés via `.env copy` est **limitée** (pas équivalent à un repo public). Ça ne te relie **pas** à l’incident ~140k profils. Reste une **mauvaise pratique** (historique Git + sujet 42) : régénérer le SECRET si tu as déjà poussé ce fichier, retirer `.env copy` du suivi Git pour l’avenir.
### Actions recommandées (à faire de ton côté)
1. **Régénérer le SECRET** sur [Applications OAuth Intra](https://profile.intra.42.fr/oauth/applications) (même si déjà fait après le chat).
2. **Retirer** `swifty_companion/.env copy` du suivi Git (`git rm --cached`, supprimer le fichier, commit).
3. Si le dépôt distant a déjà reçu ce fichier : considérer **réécriture d’historique** ou invalidation des anciens secrets (SECRET régénéré = ancien jeton mort).
4. Ne jamais committer `.env`, captures avec clés, ou dumps de profils.
---
## 5. Ce que cet audit n’a pas fait
- Pas d’appel à l’API 42 (pas de logs côté 42 consultés).
- Pas de lecture du `.env` actuel sur disque.
- Pas d’accès aux serveurs 42 / pas de corrélation avec l’enquête interne.
Seul **42** ou la police pourrait lier un compte OAuth à un volume de requêtes précis.
---
## 6. En cas de contact DPO / campus
Tu peux décrire factuellement :
- projet **Swifty Companion** (Flutter) ;
- recherche **par login** uniquement ;
- pas d’export massif dans le code livré ;
- app OAuth **pédagogique** avec scopes `public` + `projects`.
Contact mail incident : **dpo@42.tech** (comme indiqué par l’école).
---
## 7. Références
- [guide-soutenance-realisation.md](guide-soutenance-realisation.md) — cartographie sujet ↔ code
- [sujet traduit](../sujets/swifty-companion.md) — usage autorisé vs extraction massive (CGU 42)

View File

@@ -0,0 +1,178 @@
# Étape 2 — OAuth2 Intra + token unique
> **Prérequis :** étape 1 OK (`swiftyCompanion/swifty_companion/` créé, `flutter run` fonctionne).
## Objectif de l’étape
1. Obtenir un **access token** via **OAuth2 de l’Intra** (pas un token par requête API).
2. Stocker les secrets dans **`.env`** (racine `swiftyCompanion/` ou dossier `swifty_companion/` — un seul endroit, documenté dans le README).
3. Avoir un **service d’auth** réutilisable pour toutes les futures requêtes `api.intra.42.fr`.
Les **vues** (recherche login, profil) viennent aux étapes 3–4 ; ici on pose l’authentification API.
---
## 2.1 — Vérifier l’app
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion/swifty_companion
flutter pub get
flutter run -d <device>
```
L’écran compteur par défaut suffit pour valider l’étape 1.
---
## 2.2 — Créer l’application OAuth sur l’Intra
1. [profile.intra.42.fr/oauth/applications](https://profile.intra.42.fr/oauth/applications) → **New application**.
2. Renseigner :
- **Name** : ex. `swifty_companion_dev`
- **Website** : URL factice ou repo perso (souvent requis)
- **Redirect URI** : doit correspondre **exactement** à ce que l’app utilisera (voir 2.3)
3. Noter **UID** (= `CLIENT_ID`) et **SECRET** (= `CLIENT_SECRET`).
Copier `swiftyCompanion/.env.example` → `.env` (même niveau que le README rendu ou dans `swifty_companion/`) :
```env
CLIENT_ID=...
CLIENT_SECRET=...
REDIRECT_URI=...
```
---
## 2.3 — Choisir le `REDIRECT_URI` (mobile)
### Erreur Intra : « must be a valid URI »
Le formulaire **42 Intra** accepte en pratique surtout des URI **`http://` ou `https://`**.
Les schémas du type `com.example.swifty_companion://callback` sont souvent **refusés** (validateur strict + `_` dans le schéma).
**Type d’application Intra :** `42 pedagogic project` (normal pour Swifty Companion — pas de type « mobile »).
**URI de redirection** — format **http(s)://host:port/chemin** uniquement (pas de `com.example://…`).
Essayer **dans cet ordre** (une seule ligne dans le champ, sans guillemets ni espace) :
1. `http://localhost:8080/callback` ← le plus courant sur l’Intra 42
2. `http://localhost:3000/callback`
3. `http://127.0.0.1:8080/callback`
Exemple tiré de la [doc 42](https://api.intra.42.fr/apidoc/guides/web_application_flow) :
`http://localhost:1919/users/auth/ft/callback` (tu peux t’en inspirer, mais garde une URI simple type `/callback`).
Copier **exactement** la ligne acceptée dans `.env` et dans Flutter.
Avec `flutter_appauth`, Android/iOS interceptent cette URL de boucle locale après le login dans le navigateur — pas besoin d’un vrai serveur web.
| Approche | Redirect URI | Intra 42 |
|----------|--------------|----------|
| **Recommandé (Flutter + 42)** | `http://127.0.0.1:42800/callback` | ✅ en général accepté |
| Schéma custom | `com.example.swifty_companion://callback` | ❌ souvent refusé |
| Site web classique | `https://ton-domaine.com/callback` | ✅ si tu as un backend |
Le même URI doit être : formulaire Intra, `.env`, et paramètre `redirect_uri` dans l’app.
**Android** : `applicationId` = `com.example.swifty_companion` (`android/app/build.gradle.kts`) — indépendant du `REDIRECT_URI` loopback.
Endpoints 42 (référence) :
| Étape | URL |
|-------|-----|
| Autorisation | `GET https://api.intra.42.fr/oauth/authorize` |
| Token | `POST https://api.intra.42.fr/oauth/token` |
| API | `GET https://api.intra.42.fr/v2/...` avec header `Authorization: Bearer <token>` |
Paramètres authorize usuels : `client_id`, `redirect_uri`, `response_type=code`, `scope=public projects` (voir [intra-oauth-formulaire.md](intra-oauth-formulaire.md)).
---
## 2.4 — Dépendances Flutter suggérées
Dans `swifty_companion/` :
```bash
flutter pub add flutter_dotenv http
# selon le flux choisi, par ex. :
# flutter pub add flutter_appauth
```
| Package | Rôle |
|---------|------|
| `flutter_dotenv` | Charger `.env` au démarrage |
| `http` | Échange code → token + appels API |
| `flutter_appauth` | Flux OAuth2 + redirect natif (optionnel mais pratique) |
---
## 2.5 — Architecture minimale (fichiers à créer)
```
lib/
├── main.dart # dotenv.load + MaterialApp
├── config/
│ └── env.dart # lecture CLIENT_ID, etc.
└── services/
└── intra_auth_service.dart # token en mémoire (singleton)
```
Règles sujet :
- **Un seul token** : le service le garde après le login OAuth ; les futures requêtes réutilisent `getAccessToken()`.
- **Ne pas** appeler `/oauth/token` avant chaque `GET /v2/users/...`.
Pseudo-contrat du service :
```dart
class IntraAuthService {
String? _accessToken;
String? get accessToken => _accessToken;
bool get isAuthenticated => _accessToken != null;
Future<void> signIn(); // ouvre le flux OAuth, stocke le token
Future<String> getValidToken(); // retourne le token (bonus : refresh plus tard)
}
```
Test manuel étape 2 : bouton « Se connecter à l’Intra » → navigateur / WebView → retour app → afficher temporairement « Connecté » ou le login `/v2/me`.
---
## 2.6 — `.env` et assets
Si `flutter_dotenv` charge depuis la racine du package Flutter :
1. Placer `.env` dans `swifty_companion/` (ignoré par git).
2. Déclarer dans `pubspec.yaml` :
```yaml
flutter:
assets:
- .env
```
> En production soutenance : les correcteurs créent **leur** `.env` avec **leur** app OAuth ; ne jamais committer le fichier.
---
## 2.7 — Critère « terminé » pour l’étape 2
| Check | OK si |
|-------|--------|
| OAuth Intra | Connexion obtient un access token |
| Token unique | Plusieurs appels API utilisent le **même** token sans re-login |
| Secrets | Uniquement dans `.env`, pas dans le code en dur |
| Doc | README rendu rappelle où mettre `.env` et comment lancer l’app |
---
## Ensuite — étape 3
→ [etape-03-vue-recherche.md](etape-03-vue-recherche.md) (à rédiger quand l’étape 2 est validée) : **vue 1** — champ login, appel API, erreurs 404 / réseau.
Documentation API utile :
- [apidoc](https://api.intra.42.fr/apidoc)
- User : `GET /v2/users/:id` ou recherche par login (voir doc)

View File

@@ -0,0 +1,106 @@
# Étape 3 — Vue 1 : recherche par login
> **Prérequis :** étape 2 OK — OAuth actif, token unique (`IntraAuthService`).
## Objectif sujet
| Exigence | Cette étape |
|----------|-------------|
| Au moins 2 vues | **Vue 1** : recherche · **Vue 2** : profil (étape 4) |
| Erreurs gérées | Login introuvable, réseau, non connecté |
| Token unique | Réutiliser `IntraAuthService.instance` (ne pas refaire OAuth) |
---
## Parcours utilisateur (vue 1)
1. L’utilisateur est déjà connecté à l’API (étape 2) — ou bouton « Se connecter » si token absent.
2. **Champ texte** + bouton **Rechercher** (login 42, ex. `jdoe`).
3. Appel API avec `Authorization: Bearer <token>`.
4. **Succès** → navigation vers **vue 2** (profil).
5. **Échec** → message clair, pas de crash.
---
## API 42 utile
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
| Action | Requête typique |
|--------|-----------------|
| Utilisateur par login | `GET /v2/users/{login}` ou `GET /v2/users?filter[login]=<login>` |
| Toi (déjà fait) | `GET /v2/me` |
Vérifier dans l’apidoc la forme exacte supportée ; beaucoup de projets utilisent :
```http
GET https://api.intra.42.fr/v2/users?filter[login]=jdoe
Authorization: Bearer <access_token>
```
Réponse : tableau JSON ; **vide** = login inconnu.
---
## Fichiers à ajouter (étape 3)
```
lib/
├── services/
│ ├── intra_auth_service.dart # existant
│ └── intra_user_service.dart # GET user(s) avec token
├── screens/
│ ├── search_screen.dart # VUE 1
│ └── profile_screen.dart # VUE 2 (squelette → étape 4)
└── main.dart # home → SearchScreen si token OK
```
---
## `IntraUserService` (idée)
- Méthode `Future<Map<String, dynamic>> fetchUserByLogin(String login)`.
- Utilise `IntraAuthService.instance.authHeaders()`.
- **404 / liste vide** → exception métier `UserNotFoundException`.
- **Socket / timeout** → `NetworkException`.
- **401** → token expiré → message « reconnecte-toi ».
---
## Gestion des erreurs (sujet)
| Situation | Message utilisateur (exemple) |
|-----------|-------------------------------|
| Login inconnu | `Aucun utilisateur pour ce login` |
| Pas de réseau | `Connexion impossible. Vérifie le réseau.` |
| Non connecté OAuth | `Connecte-toi d’abord à l’API 42` |
| Champ vide | `Saisis un login` |
---
## Navigation
```text
SearchScreen --[login trouvé]--> ProfileScreen(login)
ProfileScreen --[retour]--> SearchScreen
```
`Navigator.push` / `pop` ou `go_router` — rester simple (Navigator 1.0 suffit).
---
## Critères « étape 3 terminée »
| # | Test |
|---|------|
| 1 | Login valide (ex. `fgras-ca`) → ouvre vue 2 |
| 2 | Login `zzzzzzzz` → message erreur |
| 3 | Mode avion → message réseau |
| 4 | Retour depuis vue 2 → nouvelle recherche possible |
| 5 | Un seul OAuth ; les recherches réutilisent le même token |
---
## Ensuite — étape 4
→ [etape-04-vue-profil.md](etape-04-vue-profil.md) : photo, ≥ 4 champs, skills, projets (dont failed), layout adaptatif.

View File

@@ -0,0 +1,121 @@
# Étape 4 — Vue 2 : profil complet
> **Prérequis :** [étape 3 — recherche](etape-03-vue-recherche.md) (navigation vers vue 2 avec un login valide).
## Objectif sujet
| Exigence | Cette étape |
|----------|-------------|
| ≥ 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 |
| Token | Un seul OAuth : `IntraAuthService` pour tous les `GET` |
| Retour vue 1 | `AppBar` + bouton retour système (`Navigator.pop`) |
| Layout | `LayoutBuilder` : colonne sur téléphone, 2 colonnes si largeur ≥ 720 px |
---
## Parcours utilisateur (vue 2)
1. Arrivée depuis la recherche avec un objet `user` minimal (`id`, `login`, parfois `image`).
2. **Chargement** : appels API en parallèle (spinner plein écran).
3. **Succès** : photo, carte « Informations », liste skills, liste projets.
4. **Échec** : message rouge + bouton **Réessayer**.
5. **Retour** : flèche AppBar → vue 1, nouvelle recherche possible.
---
## API 42 utilisée
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
| Donnée | Endpoint | Notes |
|--------|----------|--------|
| Fiche user | *(déjà renvoyée par la recherche étape 3)* | email, phone, location, wallet, image — pas de 2ᵉ `GET /users/{id}` pour limiter le quota |
| Fiche complète | `GET /v2/users/{login}` | inclut souvent `cursus_users` + `skills` par cursus |
| Niveau / skills | `cursus_users[].skills` | pas `/experiences` (souvent **403** avec token étudiant) |
| Projets | `GET /v2/users/{id}/projects_users?page[size]=100` | scope OAuth **`projects`** ; filtrer côté app : `finished` / `failed` |
Tous les appels :
```http
Authorization: Bearer <access_token>
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`) :
- **Niveau affiché** : partie entière (`8`)
- **Pourcentage** : partie décimale × 100 (`42 %`)
---
## Fichiers (étape 4)
```
lib/
├── services/
│ ├── intra_auth_service.dart # existant — headers Bearer
│ ├── intra_user_service.dart # existant — recherche
│ └── intra_profile_service.dart # NEW — agrégation profil
└── screens/
└── profile_screen.dart # StatefulWidget — chargement + UI
```
### `IntraProfileService`
- `fetchProfile(Map<String, dynamic> summaryUser)` → `IntraUserProfile`
- Requêtes **séquentielles** (pause ~450 ms) + retry sur 429 (`intra_http.dart`)
- Cache profil 2 min par `user_id` pour éviter de re-taper l’API au retour arrière
- Mêmes exceptions que l’étape 3 : `IntraNetworkException`, `IntraUnauthorizedException`, etc.
### `ProfileScreen`
- Reçoit le `user` de l’étape 3
- `initState` → `_load()`
- Pas de second OAuth
---
## Gestion des erreurs (vue 2)
| Situation | Comportement |
|-----------|----------------|
| Pas de réseau | Message + **Réessayer** |
| Token expiré (401) | « Session expirée… » — reconnecter depuis vue 1 |
| Trop de requêtes (429) | Message dédié ; retry automatique côté client + attendre avant de réessayer |
| Accès refusé (403) | Scopes `public` + `projects` sur l’app OAuth, puis **reconnexion** dans l’app |
| ID manquant | Erreur métier explicite |
| Listes vides skills / projets | Texte informatif, pas de crash |
---
## Critères « étape 4 terminée »
| # | Test |
|---|------|
| 1 | Login valide → photo + au moins 4 champs remplis ou `—` |
| 2 | Section skills avec niveau **et** pourcentage |
| 3 | Au moins un projet `finished` ou `failed` visible (si l’étudiant en a) |
| 4 | Mode avion sur vue 2 → erreur + réessai |
| 5 | Retour → vue 1 sans perdre la session OAuth |
| 6 | Largeur tablette / fenêtre large → mise en page 2 colonnes |
---
## Ensuite — étape 5
→ Affiner navigation, contraintes et polish responsive sur **les deux vues** (`etape-05-navigation-layout.md` à rédiger).

View File

@@ -0,0 +1,74 @@
# Étape 5 — Navigation et layout adaptatif
> **Prérequis :** [étape 4 — profil](etape-04-vue-profil.md) validée sur téléphone (OAuth `public` + `projects`).
## Objectif sujet
| Critère | Cette étape |
|---------|-------------|
| Retour vue 1 | `Navigator.push` / `pop` — flèche AppBar sur la vue 2 |
| Layout flexible | Scroll, marges, largeur max, 2 colonnes profil si écran large |
| Lisibilité | Rotation, clavier ouvert, petits écrans sans overflow |
---
## Navigation
```text
SearchScreen --push--> ProfileScreen
ProfileScreen --pop--> SearchScreen (session OAuth conservée)
```
- Pas de `go_router` obligatoire : **Navigator 1.0** suffit.
- La vue 1 garde le token ; une nouvelle recherche ne refait pas OAuth.
---
## Fichiers ajoutés / modifiés
```
lib/
├── theme/
│ └── swifty_ui.dart # couleur 42, padding, AppBar, scroll responsive
├── screens/
│ ├── search_screen.dart # formulaire scrollable, centré
│ └── profile_screen.dart # lignes détail adaptatives, UI partagée
└── main.dart # thème Material 3 (seed 42)
```
### `swifty_ui.dart`
| Widget / helper | Rôle |
|-----------------|------|
| `SwiftyUi.appBar` | AppBar turquoise identique sur les 2 vues |
| `ResponsiveScrollBody` | `SingleChildScrollView` + padding + `maxWidth` |
| `ErrorMessage` | Texte d’erreur sélectionnable, centré |
| `LoadingFilledButton` | Bouton avec spinner (connexion / recherche) |
### Vue recherche
- Contenu dans `ResponsiveScrollBody` (`maxWidth` ~480 px).
- Évite l’overflow quand le **clavier** est ouvert.
### Vue profil
- Breakpoint **720 px** : 1 colonne (téléphone) / 2 colonnes (tablette, paysage).
- `_DetailRow` : label + valeur en **colonne** si la ligne est trop étroite (< 400 px).
---
## Critères « étape 5 terminée »
| # | Test |
|---|------|
| 1 | Profil → retour → recherche sans déconnexion |
| 2 | Rotation paysage : pas de bande jaune / overflow |
| 3 | Clavier ouvert sur la recherche : boutons accessibles (scroll) |
| 4 | Tablette ou fenêtre large : profil en 2 colonnes lisible |
| 5 | Erreurs toujours visibles (couleur + message lisible) |
---
## Ensuite — étape 6
→ [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) : `flutter test`, démo peer-eval, préparation dépôt école.

View File

@@ -0,0 +1,118 @@
# Étape 6 — Tests, soutenance et rendu école
> **Prérequis :** étapes 2–5 OK sur device (OAuth `public` + `projects`, recherche, profil, retour, layout).
## Objectif
1. Valider la **partie obligatoire** du sujet avant peer-eval.
2. Préparer le **dépôt Vogosphere** (sans `docs/`, sans `.env`).
3. Savoir **quoi montrer** à l’évaluateur et dans quel ordre.
Référence correcteurs : [swiftyCompanion/README.md](../../swiftyCompanion/README.md).
---
## Commandes avant soutenance
Depuis le package Flutter :
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion/swifty_companion
flutter pub get
flutter analyze
flutter test
flutter doctor
```
Sur téléphone (USB ou Wi‑Fi ADB) :
```bash
flutter devices
flutter run -d <device>
```
Après changement de `.env` ou de scopes OAuth : **arrêter** `flutter run` et relancer ; **reconnexion** 42 dans l’app.
---
## Checklist peer-eval (détaillée)
| # | Test | Comment le montrer | Où c’est géré dans l’app |
|---|------|--------------------|---------------------------|
| 1 | `flutter test` | Terminal sur la machine de dev | `test/widget_test.dart` |
| 2 | Parcours nominal | Connexion → login valide → profil | `SearchScreen` → `ProfileScreen` |
| 3 | Profil complet | Photo, ≥ 4 champs, skills (niveau + %), projets validés **et** failed | `IntraProfileService` + `profile_screen.dart` |
| 4 | Login invalide | `zzzzinvalid` → message, pas de crash | `IntraUserNotFoundException` |
| 5 | Hors ligne | Mode avion sur recherche **ou** sur profil (erreur + réessai) | `IntraNetworkException`, retry profil |
| 6 | Navigation | Retour AppBar → nouvelle recherche, session conservée | `Navigator.pop` |
| 7 | Petit écran / rotation | Paysage, clavier ouvert sur la recherche | `ResponsiveScrollBody`, `swifty_ui.dart` |
| 8 | Bonus | Access invalide → refresh → recherche OK | [étape 7](etape-07-bonus-token.md) |
**Ordre de démo suggéré :** connexion → profil riche (skills + failed) → login faux → mode avion → retour → `flutter test`.
---
## Correspondance sujet v4 ↔ app
| Critère sujet | Statut attendu |
|---------------|----------------|
| 2 vues | `SearchScreen` + `ProfileScreen` |
| Erreurs | Login vide, inconnu, réseau, 401, 403, 429 |
| Token unique | `IntraAuthService.instance` partout |
| OAuth Intra | `flutter_appauth` + `.env` |
| Layout adaptatif | Étape 5 (`wideBreakpoint` 720 px) |
Champs masqués par l’API (`—`) : normal (confidentialité sujet).
---
## Rendu Git école
### Ce qui part sur Vogosphere
Racine du dépôt Intra = contenu de **`swiftyCompanion/`** :
- `README.md`, `.gitignore`, `.env.example`
- `swifty_companion/` (code Flutter)
### Ce qui ne part **pas**
- `docs/` (dépôt perso uniquement)
- `.env` (secrets)
- Fichiers de backup type `.env copy`
### Vérifier avant push
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion
git status # si repo école initialisé ici
```
- Aucun `.env` listé
- `REDIRECT_URI` dans `.env.example` = URI enregistrée sur l’Intra
- README école à jour pour les correcteurs
Workflow détaillé : [README racine](../../README.md) (section « Pousser vers l’école »).
---
## Parler en soutenance (30 s)
> « Swifty Companion : OAuth2 Intra une fois, puis recherche par login. Le profil charge cursus, skills et projects_users avec le même token. Erreurs réseau et API gérées ; layout scrollable et deux colonnes sur grand écran. Secrets dans `.env` local, non versionné. »
---
## Critères « étape 6 terminée »
| # | Fait ? |
|---|--------|
| 1 | `flutter analyze` + `flutter test` verts |
| 2 | Checklist peer-eval cochée sur device |
| 3 | `.env` absent du futur commit école |
| 4 | Tu sais lancer la démo sans notes |
---
## Ensuite — étape 7 (bonus)
→ [etape-07-bonus-token.md](etape-07-bonus-token.md) : refresh token à l’expiration (optionnel si obligatoire parfaite).

View File

@@ -0,0 +1,93 @@
# Étape 7 — Bonus : refresh token
> **Prérequis :** partie obligatoire validée ([étape 6](etape-06-checklist-soutenance.md)).
## Objectif sujet
Quand l’**access token** expire, l’app **rafraîchit** la session via le **refresh token** et continue à fonctionner **sans** relancer l’écran OAuth à chaque requête.
---
## Implémentation (ce dépôt)
| Élément | Fichier |
|---------|---------|
| Stockage `access` + `refresh` après login | `intra_auth_service.dart` |
| `POST /oauth/token` `grant_type=refresh_token` | `IntraAuthService.refreshAccessToken()` |
| Un seul refresh en parallèle | `_refreshInFlight` (évite 4 refresh si 4 GET en 401) |
| Sur **401** API → refresh → **rejouer** le GET | `intra_http.dart` → `intraApiGet(..., auth:)` |
| Parsing JSON token | `intra_oauth_tokens.dart` |
Les scopes OAuth restent `public` + `projects` à la connexion initiale.
---
## Flux
```text
signIn() → access + refresh en mémoire
GET /v2/users/... → 401
→ refreshAccessToken() → nouveaux tokens
→ même GET avec nouveau Bearer
sinon → IntraUnauthorizedException (« Reconnecte-toi »)
```
`signOut()` efface les deux tokens.
**Persistance** (secure storage) : non fait — le bonus du sujet vise surtout l’expiration **pendant** une session ; tu peux l’ajouter plus tard.
---
## Tests automatisés
```bash
cd swiftyCompanion/swifty_companion
flutter test
```
- `test/intra_oauth_tokens_test.dart` — parsing réponse `/oauth/token`
- `test/widget_test.dart` — démarrage app
---
## Test manuel (soutenance bonus)
1. Connexion 42 → recherche OK.
2. **Simuler** un access token expiré :
- en debug, appeler une recherche après avoir invalidé le token en mémoire **sans** effacer le refresh (voir ci‑dessous), **ou**
- attendre l’expiration réelle (souvent long).
3. Nouvelle recherche → refresh silencieux → profil OK **sans** repasser par le navigateur OAuth.
### Test sur device (build debug)
Sur l’écran recherche, bouton **« DEBUG : simuler token expiré »** (`kDebugMode` uniquement — absent en release).
1. Connexion 42 OK.
2. Appuyer sur le bouton debug.
3. **Rechercher** un login → refresh silencieux → profil OK sans repasser par OAuth.
En release / soutenance APK release : expliquer le flux ou montrer `flutter test`.
---
## Critères « bonus OK »
| # | Test |
|---|------|
| 1 | 401 sur API → refresh → requête réussie |
| 2 | Refresh impossible → message « session expirée », pas de crash |
| 3 | `flutter test` vert |
---
## Fichiers modifiés (récap)
```
lib/services/
├── intra_auth_service.dart # refresh + stockage refresh_token
├── intra_http.dart # 401 → refresh → retry
├── intra_oauth_tokens.dart # parse JSON token
└── … (user / profile passent auth à intraApiGet)
test/intra_oauth_tokens_test.dart
```

View File

@@ -0,0 +1,233 @@
# Fiche — dépôt de soutenance, branche `soutenance`, PC école
> **Lot 1** du [plan de préparation](00-plan-preparation-soutenance.md).
> **Validé :** branche `soutenance` = même contenu que Vogosphere ; travail quotidien sur `main` ; `.env` local uniquement ; `.env.example` dans le rendu Git.
---
## 1. Ce que dit le sujet (sécurité — **éliminatoire**)
Traduction du PDF v4, chapitre IV (encadré page 5) :
> Les identifiants, clés API, variables d’environnement, etc. doivent être stockés **localement** dans un fichier **`.env`** et **ignorés par git**. Des identifiants exposés **publiquement** entraînent **l’échec direct** du projet.
**À retenir pour le rendu :**
| Fichier | Dans le Git école (Vogosphere) ? |
|---------|----------------------------------|
| `.env` (avec vrais secrets) | **NON — jamais** |
| `.env.example` (modèle vide + explications) | **OUI** |
| `.gitignore` qui ignore `.env` | **OUI** |
Le correcteur crée **son** `.env` sur sa machine à partir de `.env.example` + **son** app OAuth Intra (ou la tienne en démo, **sans** committer les valeurs).
---
## 2. Où mettre le `.env` pour que l’app tourne
Flutter charge le fichier déclaré dans `pubspec.yaml` :
```yaml
assets:
- .env
```
→ chemin **obligatoire pour l’exécution** :
```text
swiftyCompanion/swifty_companion/.env
```
| Fichier | Rôle |
|---------|------|
| `swifty_companion/.env` | **Secrets réels** — machine locale (portable, école en démo) — **gitignore** |
| `swiftyCompanion/.env.example` | **Modèle** à la racine du paquet rendu — **versionné** |
| `swiftyCompanion/.env` (racine paquet) | **Inutile** pour Flutter — à **ne pas** utiliser pour éviter la confusion (tu peux le supprimer en local) |
**Procédure correcteur / toi :**
```bash
cd swiftyCompanion/swifty_companion
cp ../.env.example .env
# Éditer .env : CLIENT_ID, CLIENT_SECRET, REDIRECT_URI
flutter pub get
flutter run -d <device>
```
*(Le lot 2 enrichira `.env.example` avec commentaires détaillés par variable.)*
---
## 3. Contenu exact du dépôt école (Vogosphere)
**Racine du dépôt Intra** = contenu du dossier `swiftyCompanion/` (pas le repo perso entier).
```text
swiftyCompanion/ ← racine Git Vogosphere
├── README.md
├── .gitignore
├── .env.example
└── swifty_companion/ ← projet Flutter
├── pubspec.yaml
├── lib/
├── android/
├── test/
└── …
```
**Ne doit pas apparaître dans Vogosphere :**
- `docs/` (sujet traduit, pédagogie, fiches orale)
- tout `.env` avec secrets
- dépôt Gitea perso, liens vers guides internes
---
## 4. Deux Git, deux rôles
| Dépôt | Remote typique | Contenu |
|-------|----------------|---------|
| **Perso** | Gitea privé | `docs/` + `swiftyCompanion/` (travail) |
| **École** | Vogosphere | **uniquement** `swiftyCompanion/` à la racine |
Sur le **portable**, tu continues à coder sur **`main`** (perso). Quand le rendu est prêt, tu mets à jour la branche **`soutenance`** (voir §5) — c’est la référence « propre » pour présenter et pour préparer le push école.
---
## 5. Branche `soutenance` (portable)
### Objectif
- Même **arbre utile** que Vogosphere : tout sous `swiftyCompanion/` à jour et fonctionnel.
- Tu **peux** continuer à développer sur `main` ; `soutenance` ne bouge que quand tu **décides** de figer une version pour correction.
- En soutenance sur **ton PC** : tu ouvres le projet dans `swiftyCompanion/swifty_companion/` (pas besoin d’afficher `docs/` à l’écran).
### Méthode recommandée (monorepo perso)
```bash
cd ~/projets/42School/swifty_companion
# 1) Finir les modifs sur main (swiftyCompanion + éventuellement docs)
git checkout main
git status # rien de secret à committer
# 2) Mettre à jour soutenance (merge depuis main)
git checkout soutenance # créer une fois : git checkout -b soutenance
git merge main # ou cherry-pick des commits rendu uniquement
# 3) Vérifier que seul le paquet rendu est cohérent
cd swiftyCompanion/swifty_companion
flutter analyze && flutter test
# .env présent en local, absent du git :
git -C ../.. ls-files | rg '\.env' # doit montrer seulement .env.example
```
**Présentation :** `git checkout soutenance`, lancer `flutter run` depuis `swifty_companion/`.
**Travail après soutenance :** retour sur `main`, nouvelles features ; avant rendu final, re-merge vers `soutenance`.
### Alternative (si tu préfères un repo Git **uniquement** dans `swiftyCompanion/`)
- `git init` dans `swiftyCompanion/`, remote Vogosphere.
- Branche `main` = école, branche `dev` optionnelle.
- Le monorepo perso ne versionne plus le code Flutter (plus lourd à maintenir). **Tu as choisi le monorepo + branche `soutenance`** — la méthode ci-dessus suffit.
---
## 6. Ton scénario **PC école** (push Vogosphere)
C’est le flux que tu as décrit — formalisé pour ne rien oublier.
### Prérequis
- Branche `soutenance` à jour sur Gitea **ou** dossier `swiftyCompanion/` prêt sur clé USB / copie.
- `.env` **sur la clé ou la machine école** pour la démo (fichier local, **pas** dans le commit).
- URL du dépôt **Vogosphere** (Intra 42).
### Étapes sur le PC école
```bash
# A — Récupérer le travail (une fois)
cd ~
git clone https://ton-gitea/.../swifty_companion.git
cd swifty_companion
git checkout soutenance
# B — Préparer le dossier de rendu (contenu école pur)
# Option simple : le rendu EST le sous-dossier swiftyCompanion
cd ~/chemin/attendu/par/l/ecole # répertoire de rendu du sujet
rm -rf ./* # seulement si dossier vide / dédié — attention !
cp -a ~/swifty_companion/swiftyCompanion/. .
# C — Retirer le dépôt perso de la machine école (optionnel mais ton choix)
rm -rf ~/swifty_companion
# D — Git ÉCOLE uniquement dans le répertoire de rendu
cd ~/chemin/attendu/par/l/ecole
git init
git remote add origin <url-vogosphere-swifty_companion>
git add .
git status # VÉRIFIER : pas de .env, pas de docs/
git commit -m "Rendu Swifty Companion"
git push -u origin main
```
### Contrôles avant `git push` (checklist)
- [ ] `git status` ne liste **aucun** `.env`
- [ ] `.env.example` est présent et **sans** vraies clés
- [ ] Pas de dossier `docs/` du dépôt perso
- [ ] `flutter test` OK dans `swifty_companion/`
- [ ] README à la **racine** du clone Vogosphere (pas un niveau au-dessus)
---
## 7. Portable = démo = même binaire « logique » que l’école
| Action | Portable (`soutenance`) | Vogosphere |
|--------|-------------------------|------------|
| Code Flutter | `swiftyCompanion/swifty_companion/` | idem |
| Secrets | `.env` local | jamais poussé |
| Doc longue oral | `docs/pedagogie/` (perso) | absent |
| README correcteur | `swiftyCompanion/README.md` | idem |
---
## 8. Point « DEBUG » dans l’app (éclaircissement)
Ce n’est **pas** un fichier `.env` ni un secret.
Dans `search_screen.dart`, un bouton **« DEBUG : simuler token expiré »** n’apparaît que si `kDebugMode` est vrai (build **debug**, ex. `flutter run`). Il **n’existe pas** dans un APK **release** (`flutter build apk --release`).
| Question | Réponse |
|----------|---------|
| Le sujet l’interdit ? | **Non** — rien dans le PDF sur les boutons de debug. |
| Risque en soutenance ? | L’évaluateur peut le voir en `flutter run` ; tu peux l’expliquer pour le **bonus refresh**, ou le retirer avant rendu final si tu veux une UI plus « propre ». |
| Décision (lot 4) | **Garder** — le bonus sujet = refresh token ; le bouton ne fait qu’**invalider l’access** en debug pour tester le refresh sans attendre l’expiration. Absent en `flutter build apk --release`. |
→ Démo orale : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §7.
---
## 9. Commentaires en français dans le code
Le sujet n’impose pas la langue des commentaires. **Français = OK** (lot 4).
---
## 10. Prochaines étapes (autres lots)
| Lot | Document / action |
|-----|-------------------|
| 2 | README rendu + `.env.example` commenté (sans ton « IA ») |
| 3 | `fiche-soutenance-orale.md` — déroulé minute par minute + commandes |
| 4 | Commentaires `lib/` + décision finale bouton DEBUG |
---
## 11. Liens
- Plan global : [00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)
- Sujet sécurité : [swifty-companion.md](../sujets/swifty-companion.md) § IV
- PDF : [originaux/swifty_companion_subject.pdf](../sujets/originaux/swifty_companion_subject.pdf)

View File

@@ -0,0 +1,342 @@
# 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.*

View File

@@ -0,0 +1,289 @@
# Guide de correction / soutenance — ce qui a été réalisé
> **Objectif :** suivre le [sujet Swifty Companion v4](../sujets/swifty-companion.md) point par point, avec **fichiers**, **comportement** et **démo** à ne pas oublier en peer-eval.
> **Code évalué :** dépôt école = dossier [`swiftyCompanion/`](../../swiftyCompanion/) (pas `docs/`).
---
## 1. Vue d’ensemble
| Élément | Détail |
|---------|--------|
| Framework | **Flutter** (Dart), Material 3 |
| API | **42 Intra v2** — `https://api.intra.42.fr/v2` |
| Auth | **OAuth2** — `flutter_appauth` 12.x |
| Secrets | **`.env`** dans `swiftyCompanion/swifty_companion/.env` (asset, non versionné) |
| Vues | **2** — recherche (`SearchScreen`) + profil (`ProfileScreen`) |
| Bonus | **Refresh token** sur 401 + retry des requêtes API |
**Point d’entrée :** [`swiftyCompanion/swifty_companion/lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) → `home: SearchScreen()`.
---
## 2. Chapitre IV — Instructions générales & sécurité
| Exigence sujet | Réalisation | Fichiers |
|----------------|-------------|----------|
| Langage mobile (Flutter cité) | Projet Flutter standard | `swifty_companion/pubspec.yaml` |
| Dernière API 42 | Base URL v2 | [`lib/config/intra_config.dart`](../../swiftyCompanion/swifty_companion/lib/config/intra_config.dart) — `apiBaseUrl` |
| Secrets en **`.env`**, **pas dans Git** | `CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URI` | `.env` (local) · modèle [`swiftyCompanion/.env.example`](../../swiftyCompanion/.env.example) · ignore [`swiftyCompanion/.gitignore`](../../swiftyCompanion/.gitignore) et [`.gitignore` racine travail](../../.gitignore) |
| Chargement `.env` au démarrage | `dotenv.load` | [`lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) + asset dans `pubspec.yaml` (`assets: - .env`) |
**À dire en correction :** les clés OAuth ne sont jamais dans le code source ; l’évaluateur configure son propre `.env` à partir de `.env.example`.
---
## 3. Chapitre V.1 — Partie obligatoire (9 critères)
### Critère 1 — Au moins 2 vues
| Vue | Rôle | Fichier |
|-----|------|---------|
| **Vue 1** | Connexion API + recherche par login | [`lib/screens/search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) |
| **Vue 2** | Profil étudiant | [`lib/screens/profile_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart) |
**Démo :** montrer explicitement les deux écrans après une recherche réussie.
---
### Critère 2 — Gérer tous les cas d’erreur
| Situation | Message utilisateur (ex.) | Où c’est géré |
|-----------|---------------------------|---------------|
| Pas connecté OAuth | « Connecte-toi d’abord à l’API 42 » | [`intra_api_exception.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart) — `IntraNotAuthenticatedException` · [`intra_user_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart) |
| Login vide | « Saisis un login » | `IntraUserService.fetchUserByLogin` |
| Login inconnu | « Aucun utilisateur pour ce login » | `IntraUserNotFoundException` |
| Pas de réseau | « Connexion impossible. Vérifie le réseau. » | `IntraNetworkException` · `SocketException` / `ClientException` dans user + profile services |
| OAuth annulé / échec | Messages OAuth / plateforme | [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_connect()` |
| Session expirée (401) | « Session expirée. Reconnecte-toi… » | `IntraUnauthorizedException` · [`intra_http.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_http.dart) |
| Accès refusé (403) | Scopes `public` + `projects` | `IntraForbiddenException` |
| Trop de requêtes (429) | Message + attente / retry | `IntraRateLimitException` · retry dans `intra_http.dart` |
| Erreur profil (réseau, API) | Message + bouton **Réessayer** | `profile_screen.dart` — `_ErrorBody` |
| `.env` manquant | Texte explicite au lancement | `search_screen.dart` — branche `!IntraConfig.isConfigured` |
**Démo suggérée :** login `zzzzinvalid` → mode avion → (optionnel) profil en erreur + réessai.
---
### Critère 3 — Vue 2 : infos du login s’il existe
- Navigation **uniquement** si l’API renvoie un utilisateur : [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_search()` → `Navigator.push(ProfileScreen(user: …))`.
- Si liste vide → exception, **pas** d’ouverture du profil.
---
### Critère 4 — ≥ 4 détails + photo de profil
| Donnée affichée | Source API | Fichier UI |
|-----------------|------------|------------|
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
| **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` |
| **Localisation** | `user['location']` | `_DetailsCard` |
| **Wallet** | `user['wallet']` | `_DetailsCard` |
Chargement profil : [`lib/services/intra_profile_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart) — `GET /users/{login}`, puis `projects_users`.
**Note sujet :** champs masqués par l’API → affichage `—` (confidentialité OAuth).
**Non affiché (optionnel sujet) :** `evaluation_points` — tu as déjà **> 4** champs sans lui.
---
### Critère 5 — Skills : niveau + pourcentage
- **Pas** `/experiences` (souvent 403).
- Skills depuis **`cursus_users[].skills`** sur la fiche user.
- Niveau entier = partie entière du `level` API ; **%** = partie décimale × 100.
- Parsing : `IntraProfileService._parseSkillsFromCursus` · affichage : `_SkillsCard` + barre de progression.
**Démo :** lire une ligne du type `Algorithms — niveau 8 (42 %)`.
---
### Critère 6 — Projets réalisés, y compris failed
- `GET /users/{id}/projects_users?page[size]=100`
- Filtre app : statuts **`finished`** et **`failed`** — `_parseProjects` dans `intra_profile_service.dart`.
- Liste : `_ProjectsCard` (libellés Validé / Échec + note si présente).
**Scope OAuth requis :** `projects` (sinon 403).
---
### Critère 7 — Revenir à la vue 1
- `MaterialPageRoute` + **retour système** / flèche AppBar sur le profil.
- `ProfileScreen` : `SwiftyUi.appBar(..., showBack: true)`.
- Session OAuth **conservée** sur `SearchScreen` (pas de nouveau login).
**Démo :** retour → nouvelle recherche sans « Se connecter avec 42 ».
---
### Critère 8 — Layout flexible / moderne
| Mécanisme | Fichier |
|-----------|---------|
| Scroll + clavier | `ResponsiveScrollBody` — [`lib/theme/swifty_ui.dart`](../../swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart) |
| Formulaire centré (vue 1) | `searchMaxWidth` 480 px |
| Profil 2 colonnes si large | `wideBreakpoint` 720 px — `profile_screen.dart` `LayoutBuilder` |
| Lignes détail empilées si étroit | `_DetailRow` si largeur < 400 px |
| Thème 42 turquoise | `SwiftyUi.brand` · `main.dart` `ColorScheme.fromSeed` |
**Démo :** rotation paysage ou grand écran → 2 colonnes sur le profil.
---
### Critère 9 — Un token OAuth, pas un token par requête
| Règle | Implémentation |
|-------|----------------|
| Un seul login OAuth | `IntraAuthService.instance` — [`intra_auth_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart) |
| Bearer sur chaque GET | `authHeaders()` |
| Pas de OAuth à chaque recherche | `IntraUserService` / `IntraProfileService` réutilisent le même service |
**À dire :** un flux `authorizeAndExchangeCode` au début ; ensuite uniquement des `GET` API avec le même access token.
---
## 4. OAuth2 Intra (encadrés sujet)
| Élément | Valeur / choix | Fichiers |
|---------|----------------|----------|
| Authorize | `https://api.intra.42.fr/oauth/authorize` | `intra_config.dart` |
| Token | `https://api.intra.42.fr/oauth/token` | `intra_config.dart` + refresh dans `intra_auth_service.dart` |
| Scopes | `public`, `projects` | `intra_auth_service.dart` — `AuthorizationTokenRequest` |
| Redirect (device) | `com.example.swiftycompanion://callback` | `.env` · [`intra-oauth-formulaire.md`](intra-oauth-formulaire.md) |
| Android redirect | `RedirectUriReceiverActivity` | [`android/app/src/main/AndroidManifest.xml`](../../swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml) |
| Gradle scheme | `appAuthRedirectScheme` | [`android/app/build.gradle.kts`](../../swiftyCompanion/swifty_companion/android/app/build.gradle.kts) |
**Doc perso (non rendue Intra) :** [etape-02-oauth-intra.md](etape-02-oauth-intra.md), [oauth-test-manuel.md](oauth-test-manuel.md).
---
## 5. Chapitre V.2 — Bonus (refresh token)
| Exigence | Réalisation | Fichiers |
|----------|-------------|----------|
| Stocker refresh au login | `result.refreshToken` | `intra_auth_service.dart` — `signIn()` |
| Rafraîchir à expiration | `POST` `grant_type=refresh_token` | `refreshAccessToken()` |
| App continue | Sur **401** → refresh → **rejouer** le GET | `intra_http.dart` — `intraApiGet(..., auth:)` |
| Parsing JSON token | | `intra_oauth_tokens.dart` + tests |
| Test manuel debug | Bouton « DEBUG : simuler token expiré » | `search_screen.dart` (`kDebugMode` uniquement) puis **Rechercher** |
**Limite assumée :** tokens en **mémoire** seulement — après kill de l’app, reconnexion OAuth (acceptable si tu l’expliques).
**Guide détaillé :** [etape-07-bonus-token.md](etape-07-bonus-token.md).
---
## 6. Inventaire des fichiers applicatifs (lib/)
Fichiers **métier** à connaître pour la soutenance :
```
swiftyCompanion/swifty_companion/lib/
├── main.dart # App + thème + dotenv
├── config/
│ └── intra_config.dart # URLs + variables .env
├── services/
│ ├── intra_auth_service.dart # OAuth, access + refresh
│ ├── intra_user_service.dart # Recherche login
│ ├── intra_profile_service.dart # Profil agrégé
│ ├── intra_http.dart # GET + 401/429
│ ├── intra_oauth_tokens.dart # Parse token JSON (bonus)
│ └── intra_api_exception.dart # Erreurs métier
├── screens/
│ ├── search_screen.dart # Vue 1
│ └── profile_screen.dart # Vue 2
└── theme/
└── swifty_ui.dart # Layout partagé (étape 5)
```
**Tests :**
```
test/
├── widget_test.dart # Démarre app + bouton connexion
└── intra_oauth_tokens_test.dart # Parse refresh (bonus)
```
**Fichier supprimé (nettoyage) :** `auth_screen.dart` (écran test obsolète, non utilisé).
**Plateforme Android (OAuth physique) :** `AndroidManifest.xml`, `build.gradle.kts` (voir §4).
---
## 7. Endpoints API utilisés
| Usage | Méthode | Appelé depuis |
|-------|---------|---------------|
| Session courante | `GET /v2/me` | `IntraAuthService.fetchMe` (après login) |
| Recherche | `GET /v2/users?filter[login]=` | `IntraUserService` |
| Fiche complète | `GET /v2/users/{login}` | `IntraProfileService` |
| Cursus (secours) | `GET /v2/users/{id}/cursus_users` | `IntraProfileService` si pas embarqué |
| Projets | `GET /v2/users/{id}/projects_users` | `IntraProfileService` |
| Refresh token | `POST /oauth/token` | `IntraAuthService.refreshAccessToken` |
Requêtes **séquentielles** + cache profil 2 min + retry 429 : voir [etape-04-vue-profil.md](etape-04-vue-profil.md).
---
## 8. Chapitre VI — Rendu Git
| À pousser sur Vogosphere | À ne **pas** pousser |
|--------------------------|----------------------|
| Contenu de `swiftyCompanion/` | Dossier `docs/` (dépôt perso) |
| `README.md`, `.gitignore`, `.env.example` | Fichier `.env` |
| `swifty_companion/` (code Flutter) | `.env copy`, secrets dans l’historique Git |
Checklist : [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) · [swiftyCompanion/README.md](../../swiftyCompanion/README.md).
---
## 9. Script de démo (5–8 min)
1. `cd swiftyCompanion/swifty_companion` → `flutter test` (rapide).
2. `flutter run -d <device>` — montrer `.env` configuré **sans** afficher les secrets.
3. **Se connecter avec 42** — une fois.
4. Recherche login valide → **profil** : photo, champs, skills, projet failed si dispo.
5. Retour → **deuxième** recherche (même session).
6. Login invalide → message.
7. Mode avion → erreur réseau.
8. **Bonus :** DEBUG simuler token expiré → **Rechercher** → OK sans OAuth navigateur.
9. Rappeler : 2 vues, 1 token, `.env` ignoré par git.
---
## 10. Parcours pédagogique (docs perso uniquement)
| Étape | Guide |
|-------|--------|
| 0–1 | [00-reprise-et-etat.md](00-reprise-et-etat.md) |
| 2 OAuth | [etape-02-oauth-intra.md](etape-02-oauth-intra.md) |
| 3 Recherche | [etape-03-vue-recherche.md](etape-03-vue-recherche.md) |
| 4 Profil | [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| 5 Layout | [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| 6 Soutenance | [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| 7 Bonus | [etape-07-bonus-token.md](etape-07-bonus-token.md) |
| **Ce document** | **Cartographie sujet ↔ code pour la correction** |
---
## 11. Checklist « je n’ai rien oublié » (avant peer-eval)
- [ ] App OAuth Intra : scopes **public** + **projects**, redirect = `.env`
- [ ] `.env` présent sur la machine de démo, **absent** du Git école
- [ ] `flutter analyze` / `flutter test` OK
- [ ] Parcours nominal + erreurs + retour testés sur **device**
- [ ] Bonus refresh testé (DEBUG → Rechercher)
- [ ] Prêt à expliquer **où** est chaque critère du sujet (ce guide §3)
- [ ] SECRET régénéré si jamais exposé (chat, commit accidentel)
- [ ] Fichier **`.env copy`** retiré du Git — voir [audit-conformite-usage-api.md](audit-conformite-usage-api.md)
---
*Dernière alignement : sujet PDF v4 — traduction [swifty-companion.md](../sujets/swifty-companion.md).*

View File

@@ -0,0 +1,59 @@
# Formulaire OAuth Intra — valeurs pour Swifty Companion
À copier-coller sur [Nouvelle application API](https://profile.intra.42.fr/oauth/applications/new) (ou édition).
## Paramètres obligatoires
| Champ (FR) | Valeur proposée |
|------------|-----------------|
| **Nom** * | `Swifty Companion` |
| **Image** | `docs/assets/swifty_companion_oauth_icon_256.png` ou `swifty_companion_oauth_icon_256.jpg` (vrai PNG/JPEG, 256×256). Ne pas uploader l’ancien fichier mal nommé (`.png` qui était en réalité du JPEG). |
| **Description** * | `Projet pédagogique Mobile Initiation : application Flutter pour afficher le profil d’un étudiant 42 via l’API Intra.` |
| **Type d’application** * | **`42 pedagogic project`** (choix correct ; il n’y a pas de type « mobile ») |
| **Site web** | `https://www.42.fr` ou l’URL de ton dépôt GitHub / Git perso (placeholder accepté) |
| **Publique** | **Décoché** (app perso de projet) |
## URI de redirection
**Téléphone physique (recommandé Swifty)** — une ligne :
```text
com.example.swiftycompanion://callback
```
(Sans `_` dans le schéma — souvent accepté par l’Intra ; sinon voir [oauth-test-manuel.md](oauth-test-manuel.md).)
**Émulateur seulement** (optionnel) :
```text
http://localhost:8080/callback
```
**À ne pas mettre :**
- `com.example.swifty_companion://callback`
- lignes vides ou plusieurs URI mélangées
- espaces avant/après
## Champs d’application (scopes)
Cocher :
- [x] **Accéder aux données publiques de l'utilisateur** (`public`)
- [x] **Accéder aux données des projets de l'utilisateur** (`projects`) — requis pour `projects_users` (sinon **403** sur le profil)
Laisser **décoché** le reste sauf besoin explicite (équipes, médias, forum, etc.).
Après ajout du scope `projects`, **déconnexion puis nouvelle connexion** dans l’app pour obtenir un token à jour.
## Après création
Sur la page de l’application :
| Intra | `.env` |
|-------|--------|
| **UID** | `CLIENT_ID` |
| **SECRET** | `CLIENT_SECRET` |
| URI enregistrée | `REDIRECT_URI` (caractère pour caractère identique) |
Fichier : `swiftyCompanion/.env` (ne pas committer).

View File

@@ -0,0 +1,78 @@
# OAuth 42 — tests manuels des URLs
## Pourquoi pas `http://localhost` sur téléphone ?
| Environnement | `http://localhost:8080/callback` |
|---------------|----------------------------------|
| Émulateur Android | Parfois OK (localhost = l’émulateur) |
| **Téléphone physique** | ❌ « Site inaccessible » — localhost = le téléphone, pas ton PC |
**Swifty Companion (mobile réel)** : utiliser un **schéma d’app** :
```text
com.example.swiftycompanion://callback
```
À enregistrer sur l’Intra **et** dans `.env` (`REDIRECT_URI`).
---
## Endpoints 42 (référence sujet)
| Rôle | URL |
|------|-----|
| Autorisation | `https://api.intra.42.fr/oauth/authorize` |
| Token | `https://api.intra.42.fr/oauth/token` |
| API (ex. moi) | `https://api.intra.42.fr/v2/me` |
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
---
## 1. Tester l’API (sans app)
Dans un navigateur (connecté à l’Intra ou non pour cette étape) :
```text
https://api.intra.42.fr/
```
Réponse JSON attendue (message API) → l’API est joignable.
---
## 2. Tester la page d’autorisation (navigateur)
Remplace `TON_CLIENT_ID` par l’**UID** de ton app OAuth.
**Redirect actuel (mobile)** :
```text
https://api.intra.42.fr/oauth/authorize?client_id=TON_CLIENT_ID&redirect_uri=com.example.swiftycompanion%3A%2F%2Fcallback&response_type=code&scope=public
```
| Résultat | Signification |
|----------|----------------|
| Page login / autorisation 42 | ✅ URL d’auth OK |
| Erreur `redirect_uri` | ❌ URI pas enregistrée sur l’Intra ou différente de `.env` |
| Erreur `invalid_client` | ❌ `client_id` incorrect |
Après validation, le navigateur tente d’ouvrir `com.example.swiftycompanion://callback?code=...` → sur PC ça échoue (normal) ; **sur le téléphone avec l’app installée**, l’app doit s’ouvrir.
---
## 3. Alignement obligatoire (3 endroits identiques)
1. [Application OAuth Intra](https://profile.intra.42.fr/oauth/applications) → **URI de redirection**
2. `swifty_companion/.env` → `REDIRECT_URI=...`
3. Code Flutter → même valeur via `IntraConfig.redirectUri`
---
## 4. Ancien redirect (à retirer sur l’Intra si présent)
```text
http://localhost:8080/callback
```
Remplacer par `com.example.swiftycompanion://callback` (une ligne dans le formulaire).

View File

@@ -0,0 +1,200 @@
# Protocole de test manuel sur téléphone — Swifty Companion
> **Objectif :** valider la partie obligatoire (et le bonus) **sur device** avant de figer la branche `soutenance` et le rendu Vogosphere.
> **Code :** `swiftyCompanion/swifty_companion/` · **Branche travail :** `main` · **Branche portable / rendu :** `soutenance`.
**Prérequis PC :**
```bash
cd swiftyCompanion/swifty_companion
flutter pub get && flutter analyze && flutter test
flutter devices
flutter run -d <id_téléphone>
```
`.env` dans `swifty_companion/.env` · OAuth Intra : scopes **public** + **projects** · redirect = `com.example.swiftycompanion://callback`.
---
## Réponses aux points soulevés (à lire avant de cocher)
### A — Déconnexion (icône logout) vs annulation OAuth
| Action | Comportement actuel | Faut-il un message « connexion annulée » ? |
|--------|---------------------|---------------------------------------------|
| **Annuler** dans le navigateur OAuth (pendant « Se connecter avec 42 ») | Message **« Connexion annulée »** | Déjà géré — `search_screen.dart` (`FlutterAppAuthUserCancelledException`). |
| **Déconnexion** (icône logout en haut à droite quand connecté) | Retour à l’écran **« Connecte-toi »** sans message | **Non obligatoire** au sujet. C’est une **déconnexion volontaire**, pas une annulation OAuth. Optionnel : SnackBar « Déconnecté » — pas requis pour valider. |
**Test A3** du protocole = annuler **pendant** le flux navigateur, pas appuyer sur logout.
### B3 — Champs profil : `hidden`, vide, ≥ 4 détails
Le sujet demande **au moins quatre détails** parmi login, email, mobile, level, location, wallet, evaluations, etc. **+ photo**.
| Ce que tu vois | Conforme ? |
|----------------|------------|
| Email affiché | Oui — compte comme 1 détail |
| Téléphone = **`hidden`** | Oui — c’est la **valeur renvoyée par l’API** (confidentialité), pas un bug |
| Niveau cursus affiché | Oui |
| Localisation vide → **`—`** | Oui — souvent pas de poste connecté / pas sur PC école ; encadré sujet confidentialité |
| Wallet (points) | Oui |
| **Login** + **photo** (en-tête) | Comptent aussi pour le critère |
Tu as **plus de 4 lignes utiles** même si certaines sont `hidden` ou `—`. **Pas besoin d’ajouter** d’autres champs (evaluation_points, etc.) sauf choix perso.
Autres champs possibles dans l’API (non requis) : `displayname`, `correction_point`, `pool_month`, … — le sujet ne impose pas de tout afficher.
### B4 — « Niveau entier » pour les skills
L’API envoie un niveau **décimal** (ex. `8.42`).
| Affichage app | Signification |
|---------------|----------------|
| **Niveau 8** | Partie **entière** (`floor`) — c’est le « niveau entier » du sujet |
| **(42 %)** | Partie décimale × 100 — le **pourcentage** du sujet |
Exemple ligne UI : `Algorithms — niveau 8 (42 %)`.
Code : `IntraProfileService._skillFromLevel` · affichage `_SkillsCard` dans `profile_screen.dart`.
Plusieurs skills affichés = **mieux** ; le sujet exige le **format** niveau + % sur les skills montrés.
### C5 — Profil ouvert + réseau coupé (cache)
| Comportement | Explication |
|--------------|-------------|
| Profil **déjà chargé** reste visible sans réseau | Cache mémoire ~2 min (`IntraProfileService`) + données déjà en RAM — **normal** |
| Erreur réseau au **retour** puis nouvelle recherche | Couvre le critère « erreur réseau » |
| Erreur + **Réessayer** si chargement profil échoue | Déjà implémenté (`_ErrorBody`) |
Le sujet **n’exige pas** un rafraîchissement automatique du profil quand le réseau tombe. Test **C5 = optionnel** (amélioration possible plus tard, pas bloquant soutenance).
### F — Après déconnexion
Comportement attendu et **correct** :
- Plus de session → écran **connexion OAuth** uniquement ;
- **Pas** de recherche tant que tu ne t’es pas reconnecté (`IntraNotAuthenticatedException` si tu forçais une recherche — l’UI masque le formulaire tant que déconnecté).
---
## Grille de test — à cocher sur le téléphone
Légende : **O** = OK · **N** = échec · **—** = non testé / optionnel
### Préparation
| ID | Vérification | O / N / — |
|----|--------------|-----------|
| P1 | `flutter test` vert sur la branche testée | |
| P2 | `.env` présent, app démarre sans erreur config | |
| P3 | App installée via `flutter run` (debug) | |
### A — OAuth
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| A1 | **Se connecter avec 42** (succès) | Session affichée, formulaire recherche visible | |
| A2 | **Déconnexion** (icône logout) | Écran connexion, **sans** message obligatoire | |
| A3 | Reconnecter, lancer OAuth puis **annuler** dans le navigateur | Message **« Connexion annulée »** (pas confondre avec A2) | |
| A4 | Reconnexion réussie | Session OK | |
### B — Parcours nominal
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| B1 | Recherche login valide | Écran profil | |
| B2 | Photo | Image visible (ou initiale si API sans URL — expliquer à l’oral) | |
| B3 | Carte informations | ≥ 4 lignes (email, tel même `hidden`, niveau, wallet, etc.) | |
| B4 | Skills | Au moins une ligne `niveau X (Y %)` | |
| B5 | Projets | Au moins un validé et idéalement un **failed** | |
| B6 | Retour arrière | Recherche, **toujours connecté** | |
| B7 | 2ᵉ recherche (autre login) | Profil sans nouveau OAuth | |
### C — Erreurs
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| C1 | Recherche login vide | Message « Saisis un login » | |
| C2 | `zzzzinvalid` | Message, pas de profil vide | |
| C3 | Mode avion → **rechercher** | Message réseau | |
| C4 | Réseau OK → recherche à nouveau | Profil OK | |
| C5 | *(Optionnel)* Profil ouvert → couper réseau | Données en cache OK ; pas exigé par le sujet | — |
### D — Layout
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| D1 | Rotation paysage (profil) | Lisible / 2 colonnes si large | |
| D2 | Clavier sur écran recherche | Scroll OK | |
### E — Bonus (optionnel)
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| E1 | DEBUG simuler token expiré | Message debug | |
| E2 | Rechercher ensuite | Profil sans navigateur OAuth | |
### F — Session (complément)
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| F1 | Déconnecté : pas de champ recherche utilisable | Uniquement bouton connexion | |
---
## Validation sujet (après grille)
| # | Critère | Validé si… | O / N |
|---|---------|------------|-------|
| 1 | 2 vues | B1, B6 | |
| 2 | Erreurs | C1–C4 (+ A3) | |
| 3 | Profil si login existe | C2 vs B1 | |
| 4 | 4 détails + photo | B2, B3 | |
| 5 | Skills niveau + % | B4 | |
| 6 | Projets + failed | B5 | |
| 7 | Retour vue 1 | B6 | |
| 8 | Layout | D1 ou D2 | |
| 9 | Un token | B7 | |
| Bonus | Refresh | E1–E2 | |
**Décision :** si tous les **O / N** obligatoires sont **O** → passer à la **mise à jour branche `soutenance`** (voir ci-dessous).
---
## Après validation manuelle — branche `soutenance`
1. `git checkout main` → `git push origin main` (si pas déjà fait).
2. Aligner `main` / `soutenance` si besoin (ex. un seul `.env.example` sur `main`).
3. `git checkout soutenance` → vérifier commit photo (`eaacef0` ou plus récent).
4. **Re-test sur `soutenance` :**
```bash
cd swiftyCompanion/swifty_companion
flutter pub get && flutter analyze && flutter test
flutter run -d <device>
```
5. Refaire **minimum** : A1, B1–B7, C2, B2 (photo).
6. `git push origin soutenance`.
---
## Améliorations **non** bloquantes (ne pas faire avant validation sauf si tu veux)
| Idée | Priorité |
|------|----------|
| SnackBar « Déconnecté » au logout | Cosmétique |
| Pull-to-refresh profil quand réseau revient | Hors sujet |
| Afficher `evaluation_points` | Optionnel sujet |
| Invalider cache profil à la perte réseau | Optionnel |
---
## Liens
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — oral + repères code
- [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) — rendu Git
- [guide-soutenance-realisation.md](guide-soutenance-realisation.md) — sujet ↔ fichiers
*Dernière mise à jour : retours test device (logout, hidden, cache profil, skills).*

7
docs/sujets/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Sujets — Swifty Companion
| Document | Description |
|----------|-------------|
| [swifty-companion.md](swifty-companion.md) | Traduction FR complète (v4) + interprétation des visuels |
| [originaux/swifty_companion_subject.pdf](originaux/swifty_companion_subject.pdf) | PDF officiel Intra |
| [captures/](captures/) | Images extraites du PDF (couverture) |

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

View File

@@ -0,0 +1,164 @@
# Mobile Initiation — Swifty Companion
> **Version :** 4
> **Résumé :** Ce projet vise à vous initier au développement d’applications mobiles.
> **Traduction française** — source : [swifty_companion_subject.pdf](originaux/swifty_companion_subject.pdf)
---
## Table des matières
1. [Chapitre I — Avant-propos](#chapitre-i--avant-propos)
2. [Chapitre II — Introduction](#chapitre-ii--introduction)
3. [Chapitre III — Objectifs](#chapitre-iii--objectifs)
4. [Chapitre IV — Instructions générales](#chapitre-iv--instructions-générales)
5. [Chapitre V — Le projet](#chapitre-v--le-projet)
- [V.1 Partie obligatoire](#v1-partie-obligatoire)
- [V.2 Bonus](#v2-bonus)
6. [Chapitre VI — Rendu et évaluation par les pairs](#chapitre-vi--rendu-et-évaluation-par-les-pairs)
7. [Éléments visuels du PDF](#éléments-visuels-du-pdf)
---
## Page de garde (PDF page 1)
| Champ | Texte officiel (EN) | Traduction |
|-------|---------------------|------------|
| Série | Mobile Initiation | Initiation mobile |
| Titre | Swifty Companion | Swifty Companion |
| Résumé | This project aims to introduce you to the development of mobile application. | Ce projet vise à vous initier au développement d’applications mobiles. |
| Version | 4 | 4 |
### Référence visuelle — couverture
![Page de garde du sujet](captures/page-01-couverture.png)
| Zone | Description |
|------|-------------|
| Illustration centrale | Motif vertical en facettes (style low-poly), tons gris/noir — **élément graphique 42**, pas une maquette d’application |
| Fond | Motif géométrique léger (triangles) sur toute la page |
| Texte | « Mobile Initiation » puis « Swifty Companion », résumé en italique, « Version: 4 » |
Extrait haute résolution de l’illustration : [illustration-motif-42.png](captures/illustration-motif-42.png)
> **Note :** le sujet PDF (9 pages) **ne contient pas de captures d’écran de l’app à reproduire**. Les pages 2 à 9 reprennent le même fond géométrique que la couverture, avec le texte des chapitres. L’UI de votre application est définie **uniquement par la liste de critères** de la partie obligatoire.
---
## Chapitre I — Avant-propos
*(Traduction du texte Wikipedia sur le dodo — page 2 du PDF.)*
Voici ce que Wikipedia dit du **dodo** :
Le dodo (*Raphus cucullatus*) est un oiseau incapable de voler, aujourd’hui éteint, endémique de l’île Maurice, à l’est de Madagascar dans l’océan Indien. Son plus proche parent génétique était le solitaire de Rodrigues, également éteint ; les deux formaient la sous-famille des Raphinae, au sein de la famille des columbidés. Le plus proche parent encore vivant du dodo est le pigeon de Nicobar. On a un temps cru à tort qu’un dodo blanc existait sur l’île voisine de La Réunion.
Les restes subfossiles montrent que le dodo mesurait environ 1 mètre et pouvait peser 10,6 à 21,1 kg à l’état sauvage. Son apparence réelle n’est connue que par des dessins, peintures et récits du XVIIe siècle. Ces sources varient beaucoup et peu d’illustrations proviennent de spécimens vivants : l’apparence exacte reste débattue. On le représente souvent avec un plumage gris-brun, des pattes jaunes, une touffe de plumes à la queue, une tête nue grise et un bec noir, jaune et vert. Il utilisait des gastrolithes pour digérer, mangeait probablement des fruits, et vivait dans les bois des zones côtières plus sèches de Maurice. Une source mentionne un seul œuf par couvée. Il est supposé qu’il a perdu le vol face à l’abondance de nourriture et au peu de prédateurs.
Les Hollandais le mentionnent pour la première fois en 1598. Chasse, animaux domestiques et espèces invasives ont contribué à sa disparition ; la dernière observation largement acceptée date de 1662. Son extinction ne fut pas tout de suite remarquée ; certains le considéraient comme mythique. Au XIXe siècle, des recherches portèrent sur quelques spécimens rapportés en Europe au début du XVIIe siècle, dont une tête séchée — seul tissu mou encore conservé. Depuis, beaucoup de matériel subfossile a été extrait de Maurice (notamment du marais Mare aux Songes). La rapidité de l’extinction a mis en lumière le rôle de l’homme dans la disparition d’espèces entières. Le dodo est célèbre via *Alice au pays des merveilles* et est devenu un symbole d’extinction et d’obsolescence, ainsi qu’une mascotte de Maurice.
---
## Chapitre II — Introduction
Dans ce projet, vous découvrirez comment développer une **application mobile**.
L’objectif est de construire une application qui récupère les **informations des étudiants de 42** via l’**API 42**.
Une **API** (*Application Programming Interface*) est un ensemble de routines, protocoles et outils pour construire des applications. Souvent, une API se présente comme une bibliothèque avec des spécifications pour routines, structures de données, classes et variables. Dans d’autres cas — notamment les services **SOAP** et **REST** — une API est simplement une spécification d’appels distants exposés aux consommateurs.
---
## Chapitre III — Objectifs
Ce projet vise à vous familiariser avec :
- un **langage mobile** (Dart, Swift, Kotlin, Java, etc.) ;
- un **IDE** de votre choix (Android Studio est un bon candidat) ;
- des **frameworks mobiles** (le sujet cite **Flutter**) ;
- l’**API 42**.
---
## Chapitre IV — Instructions générales
- Le projet est évalué **uniquement par des humains** (soutenance).
- Vous pouvez utiliser **n’importe quel langage mobile**, en gardant à l’esprit la **dernière version de l’IDE** disponible sur les machines de l’école.
- Frameworks et bibliothèques compatibles autorisés si leur usage est **justifié** en soutenance — soyez pertinent.
- Le projet doit utiliser la **dernière version disponible de l’API 42**.
- Vos choix techniques peuvent vous rendre plus attractif sur le marché du travail.
### ⚠️ Sécurité — encadré page 5
> Pour des raisons de sécurité évidentes, identifiants, clés API, variables d’environnement, etc. doivent être stockés **localement** dans un fichier **`.env`** et **ignorés par git**. Des identifiants exposés publiquement entraînent **l’échec direct** du projet.
---
## Chapitre V — Le projet
### V.1 Partie obligatoire
Pour valider la partie obligatoire, vous devez respecter **tous** les critères suivants :
| # | Critère (EN) | Exigence en français |
|---|--------------|----------------------|
| 1 | At least 2 views | Au moins **2 vues** (écrans) |
| 2 | Handle all cases of errors | Gérer **tous** les cas d’erreur (login introuvable, erreur réseau, etc.) |
| 3 | Second view shows login info | La **2ᵉ vue** affiche les infos du login **s’il existe** |
| 4 | Four details + profile picture | Afficher **au moins 4 détails** parmi : login, email, mobile, level, location, wallet, evaluations, etc. **+ photo de profil** |
| 5 | Skills with level and percentage | Afficher les **compétences (skills)** avec **niveau** et **pourcentage** |
| 6 | Completed projects including failed | Afficher les **projets réalisés**, **y compris les échecs** |
| 7 | Navigate back to first view | Permettre de **revenir à la 1ʳᵉ vue** |
| 8 | Flexible / modern layout | Mise en page **flexible ou moderne** (contraintes, etc.) pour **plusieurs tailles d’écran** et plateformes |
| 9 | Do not create a token for each query | **Ne pas créer un token à chaque requête** — réutiliser le flux OAuth2 |
### OAuth2 — encadrés page 6
> Se référer à la **documentation OAuth2**.
> Vous devez utiliser **OAuth2 de l’Intra**. Pour des raisons de **confidentialité** et conformément aux lois de certains pays, il peut être **nécessaire de ne pas afficher** certaines informations.
**Liens utiles (hors PDF, à consulter pendant le projet) :**
- [Documentation développeur API 42](https://api.intra.42.fr/apidoc)
- [Applications OAuth sur l’Intra](https://profile.intra.42.fr/oauth/applications)
---
### V.2 Bonus
Bonus disponible :
| Bonus | Description |
|-------|-------------|
| Rafraîchissement du token | Recréer / **rafraîchir le token à l’expiration**. Si le token expire, l’app doit le renouveler et **continuer à fonctionner** dans tous les cas. |
### ⚠️ Règle bonus — encadré page 7
> La partie bonus n’est évaluée **que si la partie obligatoire est PARFAITE**. « Parfait » signifie : partie obligatoire **entièrement** réalisée et **sans dysfonctionnement**. Si **une seule** exigence obligatoire manque, le bonus **n’est pas évalué**.
---
## Chapitre VI — Rendu et évaluation par les pairs
Déposez le travail dans votre **dépôt Git** comme d’habitude. **Seul le contenu du dépôt** sera évalué en soutenance. Vérifiez les **noms de dossiers et de fichiers**.
→ Checklist soutenance (dépôt école) : [../../swiftyCompanion/README.md](../../swiftyCompanion/README.md) — dossier **non** inclus dans le rendu Intra si tu ne pousses que ce sous-répertoire.
→ Guide détaillé **sujet ↔ code** (dépôt perso) : [../pedagogie/guide-soutenance-realisation.md](../pedagogie/guide-soutenance-realisation.md)
---
## Éléments visuels du PDF
| Page PDF | Fichier extrait | Contenu visuel |
|----------|-----------------|----------------|
| 1 | [page-01-couverture.png](captures/page-01-couverture.png) | Page de garde complète |
| 1 (détail) | [illustration-motif-42.png](captures/illustration-motif-42.png) | Illustration centrale seule |
| 2–9 | *(pas d’image métier)* | Fond géométrique répété + texte des chapitres |
---
## Prochaine étape pédagogique
→ [Plan de réalisation pas à pas](../pedagogie/README.md)

View File

@@ -0,0 +1,9 @@
# Copier en .env dans CE dossier (swifty_companion/) :
# cp .env.example .env
# Puis éditer .env avec ton UID, SECRET et REDIRECT_URI Intra.
#
# Modèle détaillé (mêmes variables) : ../.env.example à la racine du dépôt.
CLIENT_ID=
CLIENT_SECRET=
REDIRECT_URI=com.example.swiftycompanion://callback

View File

@@ -5,8 +5,8 @@ Le projet s’exécute depuis ce dossier.
## Démarrage rapide
```bash
cp ../.env.example .env
# Éditer .env (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) — voir commentaires dans ../.env.example
cp .env.example .env # ou : cp ../.env.example .env
# Éditer .env (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
flutter pub get
flutter run -d <device>
```