Compare commits
6 Commits
254497935d
...
soutenance
| Author | SHA1 | Date | |
|---|---|---|---|
| eaacef0cb6 | |||
| 63886d2df5 | |||
| 7a06d891b4 | |||
| 1ce33a69d1 | |||
| 4f375e6766 | |||
| 5d79d6c688 |
55
README.md
55
README.md
@@ -1,55 +0,0 @@
|
||||
# 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)).
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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.
|
Before Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB |
@@ -1,109 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,50 +0,0 @@
|
||||
# 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.
|
||||
|
||||
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — **cartographie sujet ↔ code** pour la correction (ne rien oublier).
|
||||
|
||||
## 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.
|
||||
@@ -1,109 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,178 +0,0 @@
|
||||
# É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)
|
||||
@@ -1,106 +0,0 @@
|
||||
# É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.
|
||||
@@ -1,113 +0,0 @@
|
||||
# É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 |
|
||||
| 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).
|
||||
|
||||
### 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).
|
||||
@@ -1,74 +0,0 @@
|
||||
# É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.
|
||||
@@ -1,118 +0,0 @@
|
||||
# É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).
|
||||
@@ -1,93 +0,0 @@
|
||||
# É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
|
||||
```
|
||||
@@ -1,289 +0,0 @@
|
||||
# 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` ou `image_url` | `CircleAvatar` + `NetworkImage` — `_ProfileHeader` |
|
||||
| **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).*
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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.
|
Before Width: | Height: | Size: 870 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 204 KiB |
Binary file not shown.
@@ -1,164 +0,0 @@
|
||||
# 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
|
||||
|
||||

|
||||
|
||||
| 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)
|
||||
@@ -1,7 +1,37 @@
|
||||
# Renommer / copier en .env — ne jamais committer .env
|
||||
# Application OAuth : https://profile.intra.42.fr/oauth/applications
|
||||
# =============================================================================
|
||||
# Swifty Companion — modèle de configuration (PAS de secrets réels ici)
|
||||
# =============================================================================
|
||||
#
|
||||
# 1) Copier ce fichier vers le package Flutter :
|
||||
# cp .env.example swifty_companion/.env
|
||||
# (depuis la racine de ce dépôt, dossier swiftyCompanion/)
|
||||
#
|
||||
# 2) Remplir les valeurs depuis ton application OAuth sur l’Intra 42 :
|
||||
# https://profile.intra.42.fr/oauth/applications
|
||||
#
|
||||
# 3) Ne JAMAIS committer le fichier .env (le sujet prévoit l’échec direct si
|
||||
# des identifiants sont exposés publiquement). Ce .env.example peut être
|
||||
# versionné : il ne contient que des noms de variables et des exemples.
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
# CLIENT_ID — sur l’Intra, champ « UID » de l’application OAuth.
|
||||
# C’est l’identifiant public de ton app (pas ton login 42).
|
||||
# Exemple de forme : u-s4t2ud-xxxxxxxx...
|
||||
CLIENT_ID=
|
||||
|
||||
# CLIENT_SECRET — sur l’Intra, champ « SECRET » de l’application OAuth.
|
||||
# Sert à échanger le code OAuth contre un access token. À garder strictement
|
||||
# privé : uniquement dans .env local, jamais sur Git ni dans le chat.
|
||||
# Exemple de forme : s-s4t2ud-xxxxxxxx...
|
||||
CLIENT_SECRET=
|
||||
# Identique au champ « URI de redirection » sur l’Intra (une seule ligne, sans guillemets)
|
||||
|
||||
# REDIRECT_URI — URI de redirection enregistrée sur l’Intra (caractère pour
|
||||
# caractère identique dans le formulaire et ici).
|
||||
# Pour un téléphone Android avec ce projet : schéma custom + host callback
|
||||
# (voir aussi android/app/src/main/AndroidManifest.xml).
|
||||
REDIRECT_URI=com.example.swiftycompanion://callback
|
||||
|
||||
# Scopes à cocher sur l’Intra lors de la création de l’app (pas dans .env) :
|
||||
# - public (données publiques utilisateur)
|
||||
# - projects (liste projects_users sur le profil — sans cela, erreur 403)
|
||||
|
||||
@@ -1,87 +1,102 @@
|
||||
# Swifty Companion
|
||||
|
||||
Application mobile — profil étudiant 42 via l’**API Intra** (OAuth2).
|
||||
Application mobile Flutter : recherche d’un login 42 et affichage du profil (API Intra, OAuth2).
|
||||
|
||||
> **Dépôt de rendu école** : ce dossier est la **racine** du dépôt Git Vogosphere. Pas de `docs/` ni de sujet traduit ici (dépôt perso séparé).
|
||||
## Prérequis
|
||||
|
||||
**État :** partie obligatoire + **bonus refresh token** (401 → refresh → retry API).
|
||||
- Flutter SDK (voir [flutter.dev](https://flutter.dev))
|
||||
- Un téléphone ou émulateur Android configuré (`flutter doctor`)
|
||||
- Une application OAuth créée sur l’Intra : [Applications API](https://profile.intra.42.fr/oauth/applications)
|
||||
|
||||
## Lancer le projet
|
||||
## Installation
|
||||
|
||||
À la racine de **ce** dépôt Git (dossier qui contient `swifty_companion/`) :
|
||||
|
||||
```bash
|
||||
cd swifty_companion
|
||||
# Créer .env ici (voir .env.example à la racine du dépôt école)
|
||||
flutter pub get
|
||||
flutter run -d <device>
|
||||
cp ../.env.example .env
|
||||
```
|
||||
|
||||
Le fichier **`.env`** doit être dans **`swifty_companion/.env`** (déclaré comme asset dans `pubspec.yaml`).
|
||||
Édite `swifty_companion/.env` : renseigne `CLIENT_ID`, `CLIENT_SECRET` et `REDIRECT_URI` (voir commentaires dans `.env.example` à la racine).
|
||||
|
||||
Vérifications :
|
||||
Puis :
|
||||
|
||||
```bash
|
||||
flutter doctor
|
||||
flutter devices
|
||||
flutter pub get
|
||||
flutter run -d <identifiant_appareil>
|
||||
```
|
||||
|
||||
Lister les appareils : `flutter devices`.
|
||||
|
||||
## Configuration OAuth (résumé)
|
||||
|
||||
| Variable `.env` | Sur l’Intra 42 |
|
||||
|-----------------|----------------|
|
||||
| `CLIENT_ID` | **UID** de l’application |
|
||||
| `CLIENT_SECRET` | **SECRET** de l’application |
|
||||
| `REDIRECT_URI` | **URI de redirection** (ex. `com.example.swiftycompanion://callback`) |
|
||||
|
||||
Scopes à activer sur l’application : **public** et **projects**.
|
||||
|
||||
La redirect URI doit être la même sur l’Intra, dans `.env`, et dans la config Android du projet.
|
||||
|
||||
**Important :** le fichier `.env` reste sur ta machine uniquement. Ce dépôt contient `.env.example` sans vraies clés. Ne pousse jamais `.env` sur le Git de rendu.
|
||||
|
||||
Documentation API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Ouvrir l’app → **Se connecter avec 42** (une fois par session).
|
||||
2. Saisir un **login** → **Rechercher**.
|
||||
3. Consulter le **profil** (photo, informations, skills, projets validés ou en échec).
|
||||
4. **Retour** → nouvelle recherche avec le même token (pas de nouvelle connexion OAuth à chaque fois).
|
||||
|
||||
Certaines données peuvent s’afficher comme « — » : l’API peut masquer des champs selon la confidentialité.
|
||||
|
||||
**Photo :** l’URL est fournie par l’API (`image` sur la fiche user) ; le fichier JPEG est chargé depuis `cdn.intra.42.fr` avec le **même** access token OAuth (pas un second login).
|
||||
|
||||
## Vérifications avant soutenance
|
||||
|
||||
```bash
|
||||
cd swifty_companion
|
||||
flutter analyze
|
||||
flutter test
|
||||
```
|
||||
|
||||
## Configuration (`.env`)
|
||||
|
||||
1. App OAuth : [profile.intra.42.fr/oauth/applications](https://profile.intra.42.fr/oauth/applications)
|
||||
2. Scopes : **`public`** + **`projects`** (projets sur le profil)
|
||||
3. Redirect URI (ex. téléphone) : `com.example.swiftycompanion://callback` — identique sur l’Intra et dans `.env`
|
||||
4. Ne **jamais** committer `.env`
|
||||
|
||||
API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
|
||||
|
||||
## Parcours app
|
||||
|
||||
1. **Se connecter avec 42** (token réutilisé pour toutes les requêtes)
|
||||
2. **Rechercher** un login
|
||||
3. **Profil** : photo, champs, skills (niveau + %), projets validés et failed
|
||||
4. **Retour** → nouvelle recherche
|
||||
|
||||
## Partie obligatoire (rappel sujet v4)
|
||||
## Critères du sujet (v4) couverts par l’app
|
||||
|
||||
| # | Exigence |
|
||||
|---|----------|
|
||||
| 1 | Au moins **2 vues** |
|
||||
| 2 | **Toutes** les erreurs gérées (login inconnu, réseau, …) |
|
||||
| 3 | Vue 2 : infos du login s’il **existe** |
|
||||
| 4 | **≥ 4 détails** + **photo de profil** |
|
||||
| 5 | **Skills** : niveau + pourcentage |
|
||||
| 6 | **Projets** réalisés, **y compris les failed** |
|
||||
| 7 | Retour possible vers la **vue 1** |
|
||||
| 8 | Layout **adaptatif** |
|
||||
| 9 | **OAuth2** — **un token** réutilisé |
|
||||
|
||||
Certaines données peuvent être masquées par l’API (confidentialité).
|
||||
| 1 | Deux écrans : recherche et profil |
|
||||
| 2 | Erreurs gérées (login inconnu, réseau, session, etc.) |
|
||||
| 3 | Profil affiché si le login existe |
|
||||
| 4 | Au moins quatre informations + photo |
|
||||
| 5 | Skills avec niveau et pourcentage |
|
||||
| 6 | Projets terminés, y compris en échec |
|
||||
| 7 | Retour vers l’écran de recherche |
|
||||
| 8 | Mise en page adaptée (scroll, grands écrans) |
|
||||
| 9 | Un seul flux OAuth, token réutilisé pour les appels API |
|
||||
|
||||
## Bonus
|
||||
|
||||
- Refresh token : `IntraAuthService.refreshAccessToken()` + retry dans `intraApiGet` sur 401.
|
||||
Rafraîchissement du token d’accès lorsque l’API renvoie 401, puis nouvelle tentative de la requête (sans refaire l’écran de connexion 42).
|
||||
|
||||
## Soutenance — checklist peer-eval
|
||||
## Déroulé de démonstration (peer-eval)
|
||||
|
||||
| # | Test | Démo |
|
||||
|---|------|------|
|
||||
| 1 | `flutter test` | Tests OK |
|
||||
| 2 | `flutter run -d <device>` | Vue 1 → login valide → vue 2 |
|
||||
| 3 | Profil | Photo + ≥ 4 champs + skills + projets (dont failed) |
|
||||
| 4 | Login invalide | Message clair, pas de crash |
|
||||
| 5 | Réseau coupé | Erreur réseau gérée |
|
||||
| 6 | Navigation | Retour vue 1, nouvelle recherche |
|
||||
| 7 | Petit écran / rotation | UI lisible |
|
||||
| 8 | Bonus | Refresh token si implémenté |
|
||||
1. `flutter test`
|
||||
2. `flutter run` → connexion 42 → login valide → profil complet
|
||||
3. Login invalide → message d’erreur
|
||||
4. Mode avion (ou réseau coupé) → message réseau
|
||||
5. Retour à la recherche → nouvelle recherche
|
||||
6. Rotation ou petit écran → interface lisible
|
||||
7. Bonus : expliquer ou montrer le refresh token si demandé
|
||||
|
||||
**Ordre suggéré :** connexion → profil → login faux → hors ligne → retour → `flutter test`.
|
||||
## Structure du dépôt
|
||||
|
||||
## Fichiers de ce dépôt
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---------|------|
|
||||
| `README.md` | Ce document |
|
||||
| `.gitignore` | Exclut `.env`, build, etc. |
|
||||
| `.env.example` | Modèle sans secrets |
|
||||
| `swifty_companion/` | Projet Flutter (`lib/`, `android/`, …) |
|
||||
```text
|
||||
.
|
||||
├── README.md # ce fichier
|
||||
├── .gitignore
|
||||
├── .env.example # modèle (sans secrets)
|
||||
└── swifty_companion/ # code Flutter (lib/, android/, test/, …)
|
||||
```
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
# swifty_companion
|
||||
# Package Flutter — Swifty Companion
|
||||
|
||||
A new Flutter project.
|
||||
Le projet s’exécute depuis ce dossier.
|
||||
|
||||
## Getting Started
|
||||
## Démarrage rapide
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
```bash
|
||||
cp ../.env.example .env
|
||||
# Éditer .env (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) — voir commentaires dans ../.env.example
|
||||
flutter pub get
|
||||
flutter run -d <device>
|
||||
```
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
Instructions complètes (OAuth, soutenance, structure du dépôt) : **README.md à la racine du dépôt** (dossier parent).
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Point d'entrée : charge les secrets depuis `.env` (asset, non versionné) puis vue recherche.
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../services/intra_api_exception.dart';
|
||||
import '../services/intra_auth_service.dart';
|
||||
import '../services/intra_profile_service.dart';
|
||||
import '../theme/swifty_ui.dart';
|
||||
|
||||
/// Vue 2 — profil complet (étape 4).
|
||||
/// Vue 2 — photo, ≥ 4 champs, skills (niveau + %), projets validés et failed.
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key, required this.user});
|
||||
|
||||
@@ -56,11 +60,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extrait l’URL photo depuis la fiche `GET /v2/users/{login}` (critère sujet : photo de profil).
|
||||
///
|
||||
/// Ordre : `image.link` → `image.versions` (large…micro) → champ legacy `image_url`.
|
||||
String? _imageUrl(Map<String, dynamic> user) {
|
||||
final image = user['image'];
|
||||
if (image is Map<String, dynamic>) {
|
||||
final link = image['link'];
|
||||
if (link is String && link.isNotEmpty) return link;
|
||||
|
||||
final versions = image['versions'];
|
||||
if (versions is Map<String, dynamic>) {
|
||||
for (final key in ['large', 'medium', 'small', 'micro']) {
|
||||
final v = versions[key];
|
||||
if (v is String && v.isNotEmpty) return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
final url = user['image_url'];
|
||||
if (url is String && url.isNotEmpty) return url;
|
||||
@@ -172,6 +187,71 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Avatar profil : charge l’image via HTTP avec le Bearer OAuth (`IntraAuthService.cdnImageHeaders`).
|
||||
///
|
||||
/// Un simple `NetworkImage(url)` sans token échoue souvent sur le CDN 42. Si le chargement
|
||||
/// échoue ou l’URL est absente, affiche l’initiale du login (conforme encadré confidentialité sujet).
|
||||
class _IntraProfileAvatar extends StatefulWidget {
|
||||
const _IntraProfileAvatar({required this.login, required this.imageUrl});
|
||||
|
||||
final String login;
|
||||
final String? imageUrl;
|
||||
|
||||
@override
|
||||
State<_IntraProfileAvatar> createState() => _IntraProfileAvatarState();
|
||||
}
|
||||
|
||||
class _IntraProfileAvatarState extends State<_IntraProfileAvatar> {
|
||||
Uint8List? _bytes;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _IntraProfileAvatar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||
_bytes = null;
|
||||
_load();
|
||||
}
|
||||
}
|
||||
|
||||
/// GET binaire sur l’URL CDN ; même session que les appels `api.intra.42.fr`.
|
||||
Future<void> _load() async {
|
||||
final url = widget.imageUrl;
|
||||
if (url == null || url.isEmpty) return;
|
||||
|
||||
final headers = IntraAuthService.instance.cdnImageHeaders();
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url), headers: headers);
|
||||
if (response.statusCode == 200 && mounted) {
|
||||
setState(() => _bytes = response.bodyBytes);
|
||||
}
|
||||
} catch (_) {
|
||||
// Initiale affichée en repli.
|
||||
}
|
||||
}
|
||||
|
||||
Widget _initial() {
|
||||
final letter =
|
||||
widget.login.isNotEmpty ? widget.login[0].toUpperCase() : '?';
|
||||
return Text(letter, style: const TextStyle(fontSize: 32));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CircleAvatar(
|
||||
radius: 56,
|
||||
backgroundImage:
|
||||
_bytes != null ? MemoryImage(_bytes!) : null,
|
||||
child: _bytes == null ? _initial() : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorBody extends StatelessWidget {
|
||||
const _ErrorBody({required this.message, required this.onRetry});
|
||||
|
||||
@@ -206,17 +286,7 @@ class _ProfileHeader extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 56,
|
||||
backgroundImage:
|
||||
imageUrl != null ? NetworkImage(imageUrl!) : null,
|
||||
child: imageUrl == null
|
||||
? Text(
|
||||
login.isNotEmpty ? login[0].toUpperCase() : '?',
|
||||
style: const TextStyle(fontSize: 32),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
_IntraProfileAvatar(login: login, imageUrl: imageUrl),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
login,
|
||||
|
||||
@@ -186,6 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
loading: _searching,
|
||||
onPressed: _search,
|
||||
),
|
||||
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
|
||||
@@ -125,6 +125,16 @@ class IntraAuthService {
|
||||
};
|
||||
}
|
||||
|
||||
/// En-têtes pour télécharger la photo sur `cdn.intra.42.fr`.
|
||||
///
|
||||
/// L’URL vient du JSON user (`image.link` / `versions`). Le CDN refuse souvent
|
||||
/// les requêtes sans Bearer (403). On réutilise le **même** access token que pour
|
||||
/// l’API — pas un second OAuth. On n’envoie pas `Accept: application/json` ici.
|
||||
Map<String, String> cdnImageHeaders() {
|
||||
if (!isAuthenticated) return const {};
|
||||
return {'Authorization': 'Bearer $_accessToken'};
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchMe() async {
|
||||
final response = await intraApiGet(
|
||||
Uri.parse('${IntraConfig.apiBaseUrl}/me'),
|
||||
@@ -139,8 +149,7 @@ class IntraAuthService {
|
||||
_refreshToken = refresh;
|
||||
}
|
||||
|
||||
/// Simule un access token expiré (garde le refresh) — tests bonus en debug.
|
||||
@visibleForTesting
|
||||
/// Simule un access token expiré (garde le refresh) — démo bonus, appelé depuis l’UI en `kDebugMode`.
|
||||
void debugInvalidateAccessToken() {
|
||||
if (!canRefresh) return;
|
||||
_accessToken = 'invalid_access_token_for_refresh_test';
|
||||
|
||||
@@ -50,6 +50,7 @@ Duration _retryDelay(http.Response response, int attempt) {
|
||||
return Duration(milliseconds: ms.clamp(800, 8000));
|
||||
}
|
||||
|
||||
/// Lance une [IntraApiException] si le code HTTP n'est pas 200.
|
||||
void ensureIntraOk(http.Response response) {
|
||||
if (response.statusCode == 401) {
|
||||
throw const IntraUnauthorizedException();
|
||||
@@ -67,6 +68,7 @@ void ensureIntraOk(http.Response response) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Décode le corps JSON après contrôle du statut HTTP.
|
||||
dynamic decodeIntraBody(http.Response response) {
|
||||
ensureIntraOk(response);
|
||||
return jsonDecode(response.body);
|
||||
|
||||
@@ -55,10 +55,12 @@ class _ProfileCacheEntry {
|
||||
final DateTime fetchedAt;
|
||||
}
|
||||
|
||||
/// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`).
|
||||
class IntraProfileService {
|
||||
IntraProfileService._();
|
||||
static final IntraProfileService instance = IntraProfileService._();
|
||||
|
||||
/// Cache court + délai entre GET pour limiter les 429 sur l'API 42.
|
||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
||||
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'intra_api_exception.dart';
|
||||
import 'intra_auth_service.dart';
|
||||
import 'intra_http.dart';
|
||||
|
||||
/// Recherche d'un login via `GET /v2/users?filter[login]=` (même token OAuth que le profil).
|
||||
class IntraUserService {
|
||||
IntraUserService._();
|
||||
static final IntraUserService instance = IntraUserService._();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Constantes UI partagées (étape 5 — layout).
|
||||
/// Constantes UI partagées (layout responsive, thème 42).
|
||||
abstract final class SwiftyUi {
|
||||
static const Color brand = Color(0xFF00BABC);
|
||||
|
||||
@@ -66,6 +66,7 @@ class ResponsiveScrollBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Message d'erreur centré (recherche ou profil).
|
||||
class ErrorMessage extends StatelessWidget {
|
||||
const ErrorMessage({super.key, required this.message});
|
||||
|
||||
@@ -81,6 +82,7 @@ class ErrorMessage extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Bouton principal avec indicateur de chargement.
|
||||
class LoadingFilledButton extends StatelessWidget {
|
||||
const LoadingFilledButton({
|
||||
super.key,
|
||||
|
||||
Reference in New Issue
Block a user